# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 """Définition des blocs de base: objets, liens, prédicats """ __all__ = ( 'XT', 'listP', 'fileP', 'pathP', 'mpathP', 'lowerP', 'catalog', 'ADD_UNIQUE', 'ADD', 'REMOVE', 'RESET_ADD', 'Link', 'Object', 'Fact', ) import os from os import path from glob import glob from .utils import * from .expr import Expr # méthodes de mise à jour des attributs ADD_UNIQUE = object() ADD = object() REMOVE = object() RESET_ADD = object() # valeurs marqeurs pour les méthodes qui doivent pouvoir utiliser None comme # valeur _RAISE_EXCEPTION = object() _UNDEF = object() # fonctions diverses def isindex(k): return isnum(k) or isinstance(k, slice) def XT(dictOrClass, **kw): """créer un objet dictionnaire avec la valeur Dict ou Class.ATTRS augmenté des valeurs de **kw """ if isinstance(dictOrClass, dict): d = dictOrClass else: d = dictOrClass.ATTRS return dict(d, **kw) # parsers class ListP(object): """parser qui prend une liste de valeurs séparées par des virgules """ def parse(self, l): if l is None: return None return flattenstr(listof(l)) listP = ListP() class FileP(object): """parser qui prend une liste de fichiers séparées par ':' les chemins sont tilde-expansés """ def parse(self, f): fs = flattenstr(listof(f), sep=':') return [path.expanduser(f) for f in fs] fileP = FileP() class PathP(object): """parser qui prend une liste de chemins séparées par ':' les chemins sont tilde-expansés et transformés en chemins absolus """ def parse(self, p): ps = flattenstr(listof(p), sep=':') return [path.abspath(path.expanduser(p)) for p in ps] pathP = PathP() class MpathP(object): """parser qui prend une liste de chemins séparées par ':' les chemins sont tilde-expansés et transformés en chemins absolus, mais uniquement s'ils contiennent un caractère '/' """ def parse(self, p): ps = flattenstr(listof(p), sep=':') pms = [] for p in ps: pm = path.expanduser(p) if '/' in pm: pm = path.abspath(pm) pms.append(pm) return pms mpathP = MpathP() class LowerP(object): """parser qui transforme en minuscule les valeurs """ def parse(self, s): if s is None: return None return s.lower() lowerP = LowerP() ################################################################################ # Catalogue des objets class Catalog(object): # map otype --> oclass _t2cmap, t2cmap = None, property(lambda self: self._t2cmap) # map otype --> id2object _t2iomap, t2iomap = None, property(lambda self: self._t2iomap) # liste d'instance de facts _facts, facts = None, property(lambda self: self._facts) # types d'objets qui doivent être résolus en premier _rf_otypes = None def __init__(self): self._t2cmap = {} self._t2iomap = {} self._facts = [] self._rf_otypes = set() def register(self, oclass, otype=None, resolve_first=False): if otype is None: otype = oclass.__name__.lower() self._t2cmap[otype] = oclass self._rf_otypes.add(otype) ############################################################################ # Création def create_object(self, otype, oid, *values, **attrs): """créer un nouvel objet du type spécifié ou le mettre à jour """ if isinstance(otype, type): oclass = otype otype = otype.TYPE else: oclass = self._t2cmap.get(otype, None) if not self._t2iomap.has_key(otype): self._t2iomap[otype] = {} i2omap = self._t2iomap[otype] if not i2omap.has_key(oid): if oclass is None: object = Object(oid, *values, **attrs) object.otype = otype else: object = oclass(oid, *values, **attrs) i2omap[oid] = object else: object = i2omap[oid] if values: object.update('values', values) if attrs: object.update(attrs) return object def create_fact(self, sotype, soid, verb, totype, toid, **attrs): """créer un nouveau fait entre les deux objets spécifiés """ if isinstance(sotype, type): sotype = sotype.TYPE if isinstance(totype, type): totype = totype.TYPE fact = Fact(sotype, soid, verb, totype, toid, **attrs) self._facts.append(fact) return fact def resolve(self): """résoudre tous les objets et tous les faits """ rf_otypes = self._rf_otypes # d'abord résoudre les types d'objets mentionnés dans rf_otypes for otype in rf_otypes: i2omap = self.t2iomap.get(otype, None) if i2omap is None: continue for id, object in i2omap.items(): object.resolve(self) # puis résoudre les autres types d'objets for otype, i2omap in self.t2iomap.items(): if otype in rf_otypes: continue for id, object in i2omap.items(): object.resolve(self) # puis résoudre tous les faits for fact in self.facts: fact.resolve(self) return self ############################################################################ # Consultation def get(self, otype, oid, default=_RAISE_EXCEPTION, create=True, resolve=True): """obtenir un objet par son type et son identifiant par défaut, le créer s'il n'existe pas. avec create=True, l'argument default est ignoré. si create=False, default indique la valeur à retourner. lancer une exception ValueError si default=_RAISE_EXCEPTION (c'est la valeur par défaut) """ object = None i2omap = self._t2iomap.get(otype, None) if i2omap is not None: object = i2omap.get(oid, None) if object is None and not create: if default is _RAISE_EXCEPTION: raise ValueError("%s:%s: not found" % (otype, oid)) else: return default if object is None: object = self.create_object(otype, oid) if resolve: object.resolve(self) return object ############################################################################ # Recherches def find_tobjects(self, totype, objects, create=True, resolve=True): """trouver les objets liés de type totype dans la objects """ objects = listof(objects) if totype is not None: # mettre dans un dictionnaire et indexer sur oid pour éviter les # doublons tobjects = {} for object in objects: if object.otype == totype: tobjects[object.oid] = object else: lobjects = [link.resolve(self, None, create, resolve) for link in object.get_links(totype)] for lobject in lobjects: if lobject is None: continue tobjects[lobject.oid] = lobject objects = tobjects.values() return objects def filter_objects(self, expr, objects): """ne garder dans la liste objects que les objets qui correspondent à l'expression. """ objects = listof(objects) return [object for object in objects if Expr.match_term(expr, object)] def find_objects(self, otype=None, oid=None, totype=None, expr=None, create=True, resolve=True): """chercher les objets correspondant à otype et/ou oid si totype!=None, alors chercher les objets liés qui sont de ce type """ otypes = listof(otype, None) oids = listof(oid, None) if otypes is not None and oids is not None: objects = [] for otype in otypes: i2omap = self.t2iomap.get(otype, {}) objects.extend([object for object in i2omap.values() if object.oid in oids]) elif otypes is not None and oids is None: objects = [] for otype in otypes: i2omap = self.t2iomap.get(otype, {}) objects.extend(i2omap.values()) elif oids is not None and otypes is None: objects = [] for otype, i2omap in self.t2iomap.items(): objects.extend([object for object in i2omap.values() if object.oid in oids]) else: objects = [] for otype, i2omap in self.t2iomap.items(): objects.extend(i2omap.values()) if resolve: map(lambda object: object.resolve(self), objects) objects = self.find_tobjects(totype, objects, create, resolve) if expr is not None: objects = self.filter_objects(expr, objects) return objects def filter_facts(self, expr, facts): """ne garder dans la liste facts que les faits qui correspondent à l'expression """ facts = listof(facts) return [(fact, tsobjects, ttobjects) for (fact, tsobjects, ttobjects) in facts if Expr.match_term(expr, fact)] def find_facts(self, sotype=None, soid=None, verb=None, totype=None, toid=None, tsotype=None, tsexpr=None, ttotype=None, ttexpr=None, expr=None, resolve=True): """chercher les faits correspondant aux arguments retourner une liste de tuples (fact, tsobjects, ttobjects) où * fact est le fait original * tsobjects sont les objets sources liés si tsotype et tsexpr sont spécifiés * ttobjects sont les objets destination liés si ttotype et ttexpr sont spécifiés """ sotypes = listof(sotype, None) soids = listof(soid, None) verbs = listof(verb, None) totypes = listof(totype, None) toids = listof(toid, None) facts = [] for fact in self.facts: if sotypes is not None and fact.sotype not in sotypes: continue if soids is not None and fact.soid not in soids: continue if verbs is not None and fact.verb not in verbs: continue if totypes is not None and fact.totype not in totypes: continue if toids is not None and fact.toid not in toids: continue tsobjects = [fact.sresolve(self, None, True)] ttobjects = [fact.tresolve(self, None, True)] if tsotype is not None: # chercher les objets liés dans la source tsobjects = self.filter_objects(tsexpr, self.find_tobjects(tsotype, tsobjects)) if not tsobjects: continue if ttotype is not None: # chercher les objets liés dans la source ttobjects = self.filter_objects(ttexpr, self.find_tobjects(ttotype, ttobjects)) if not ttobjects: continue facts.append((fact, tsobjects, ttobjects)) if resolve: for fact, tsobjects, ttobjects in facts: fact.resolve(self) if expr is not None: facts = self.filter_facts(expr, facts) return facts ############################################################################ # Divers def dump(self): self.resolve() for otype, i2omap in self.t2iomap.items(): print "OBJECTS:%s:" % otype for id, object in i2omap.items(): object.dump(" ") if self.facts: print "FACTS:" for fact in self.facts: fact.dump(" ") ################################################################################ # liens class Link(object): """Un lien vers une référence d'un objet Un lien a le type de l'objet cible (propriété `otype`), son identifiant (propriété `oid`), et des attributs multivalués (toutes les autres propriétés) """ ATTRS = dict(otype=None, oid=None, attrs=None) _rw_attrs = set(('otype', 'oid')) _ro_attrs = set(('attrs',)) _reserved_attrs = _rw_attrs | _ro_attrs _otype = None _oid = None _attrs = None def __init__(self, otype=None, oid=None, **attrs): self.__dict__['_otype'] = otype self.__dict__['_oid'] = oid self.__dict__['_attrs'] = {} for attr, value in attrs.items(): self.update(attr, value) def __parse(self, attr, value): """obtenir le parser qui permet de s'assurer que value est dans le bon format pour l'attribut attr. """ if isindex(attr): parser = None else: parser = self.ATTRS.get(attr, None) if parser is None: return value elif isseq(value): return flattenseq(map(parser.parse, value)) else: return parser.parse(value) # accès aux attributs def __getattr__(self, attr): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) try: return self._attrs[attr] except KeyError: raise AttributeError(attr) def __setattr__(self, attr, value): value = self.__parse(attr, value) if attr in self._rw_attrs: return super(Link, self).__setattr__('_%s' % attr, value) elif attr in self._ro_attrs: raise AttributeError(attr) else: self._attrs[attr] = listof(value) def __delattr__(self, attr): if attr in self._reserved_attrs: raise AttributeError(attr) try: del self._attrs[attr] except KeyError: raise AttributeError(attr) def __getitem__(self, attr): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) else: return self._attrs[attr] def __setitem__(self, attr, value): value = self.__parse(attr, value) if attr in self._rw_attrs: return super(Link, self).__setattr__('_%s' % attr, value) elif attr in self._ro_attrs: raise KeyError(attr) else: self._attrs[attr] = listof(value) def __delitem__(self, attr): if attr in self._reserved_attrs: raise KeyError(attr) else: del self._attrs[attr] def first(self, attr, default=None): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) if self._attrs.has_key(attr): values = self._attrs[attr] if values: return values[0] return default def get(self, attr, default=None): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) else: return self._attrs.get(attr, default) def has_key(self, attr): """tester l'existence d'un attribut""" if attr in self._reserved_attrs: return True else: return self._attrs.has_key(attr) @property def known_attrs(self): """obtenir une liste triée d'attributs faisant partie du schéma""" return sorted(list(self.ATTRS.keys())) @property def misc_attrs(self): """obtenir une liste triée d'attributs ne faisant pas partie du schéma""" schema_attrs = set(self.ATTRS.keys()) defined_attrs = set(self._attrs.keys()) return sorted(list(defined_attrs - schema_attrs)) @property def missing_attrs(self): """obtenir une liste triée d'attributs faisant partie du schéma mais non définis""" schema_attrs = set(self.ATTRS.keys()) defined_attrs = set(self._attrs.keys()) return sorted(list(schema_attrs - defined_attrs - self._reserved_attrs)) def update(self, attr, value=None, update_type=ADD_UNIQUE): """mettre à jour l'attribut spécifié si l'attribut n'existe pas, il est créé. sinon, la liste des valeurs de l'attribut est étendue. si value==None, aucune mise à jour n'est effectuée si attr est une instance de dictionnaire, mettre à jour *tous* les attributs spécifiés. update_type est la méthode de mise à jour """ if isinstance(attr, dict): attrs = attr for attr, value in attrs.items(): self.update(attr, value, update_type) return self if attr in self._reserved_attrs: raise KeyError(attr) if value is not None: values = listof(self.__parse(attr, value)) if not self._attrs.has_key(attr): self._attrs[attr] = [] attr = self._attrs[attr] if update_type is ADD_UNIQUE: for value in values: if value not in attr: attr.append(value) elif update_type is ADD: attr.extend(values) elif update_type is REMOVE: for value in values: if value in attr: attr.remove(value) elif update_type is RESET_ADD: attr[:] = values return self def set_defaults(self, attr, value=None, update_type=ADD_UNIQUE): """Mettre à jour l'attribut spécifié s'il n'existe pas si value==None, aucune mise à jour n'est effectuée si attr est une instance de dictionnaire, mettre à jour *tous* les attributs spécifiés s'ils n'existent pas. """ if isinstance(attr, dict): attrs = attr for attr, value in attrs.items(): self.set_defaults(attr, value, update_type) return self if attr in self._reserved_attrs: raise KeyError(attr) if not self._attrs.has_key(attr): self.update(attr, value, update_type) return self def clone(self): """cloner ce lien""" return self.__class__(self._otype, self._oid, **self._attrs) # catalogue def resolve(self, catalog, default=_RAISE_EXCEPTION, create=True, resolve=True): """obtenir l'objet lié """ return catalog.get(self.otype, self.oid, default, create, resolve) # divers def _dump_idtype(self, indent, prefix=None): if prefix is None: prefix = '' else: prefix = "%s " % prefix print "%s%s%s:%s" % (indent, prefix, self._otype, self._oid) def _dump_attrs(self, indent): attrs = self._attrs missing_attrs = self.missing_attrs if attrs or missing_attrs: print "%s attrs:" % indent for name, values in attrs.items(): if len(values) == 1: print "%s %s=%s" % (indent, name, repr(values[0])) else: print "%s %s=(%s)" % (indent, name, ', '.join(map(repr, values))) for name in missing_attrs: print "%s %s=" % (indent, name) def dump(self, indent='', prefix=None): """Afficher l'identifiant, le type et les attributs de ce lien """ self._dump_idtype(indent, prefix) self._dump_attrs(indent) ################################################################################ # objets class MetaObject(type): def __init__(cls, name, bases, attrs): type.__init__(cls, name, bases, attrs) if cls.__dict__.get('TYPE', None) is None: cls.TYPE = cls.__name__.lower() register = not cls.__dict__.get('__NO_AUTO_REGISTER__', False) resolve_first = cls.__dict__.get('__RESOLVE_FIRST__', False) if register: catalog.register(cls, cls.TYPE, resolve_first) class Object(object): """Un objet générique Un objet a un identifiant (propriété `oid`), un type (propriété `otype`), une liste de valeurs (propriété `values`), des liens vers d'autres objets (propriété `links`) et des attributs multivalués (toutes les autres propriétés). Le type de l'objet définit un schéma, c'est à dire un ensemble d'attributs spécifiques avec des valeurs par défaut. Les attributs du schéma sont les attributs connus (propriété known_attrs), les autres sont les attributs divers (propriété misc_attrs) """ __metaclass__ = MetaObject __NO_AUTO_REGISTER__ = True __RESOLVE_FIRST__ = False ATTRS = dict(otype=None, oid=None, values=None, attrs=None, links=None) TYPE = 'object' _rw_attrs = set(('otype', 'oid')) _ro_attrs = set(('values', 'attrs', 'links')) _reserved_attrs = _rw_attrs | _ro_attrs _otype = None _oid = None _values = None _attrs = None _links = None _resolved = None def __init__(self, oid=None, *values, **attrs): self.__dict__['_otype'] = self.TYPE self.__dict__['_oid'] = oid self.__dict__['_values'] = [] self.__dict__['_attrs'] = {} self.__dict__['_links'] = {} self.__dict__['_resolved'] = False self.update('values', values) for attr, value in attrs.items(): self.update(attr, value) def __parse(self, attr, value): """obtenir le parser qui permet de s'assurer que value est dans le bon format pour l'attribut attr. Utiliser attr==None pour l'attribut values """ if isindex(attr): attr = 'values' parser = self.ATTRS.get(attr, None) if parser is None: return value elif isseq(value): return flattenseq(map(parser.parse, value)) else: return parser.parse(value) # accès aux valeurs (via un index numérique) et aux attributs (via le nom de # l'attribut) def __getattr__(self, attr): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) try: if isindex(attr): return self._values[attr] else: return self._attrs[attr] except KeyError: raise AttributeError(attr) def __setattr__(self, attr, value): value = self.__parse(attr, value) if attr == 'values': self._values[:] = listof(value) elif attr in self._rw_attrs: super(Object, self).__setattr__('_%s' % attr, value) elif attr in self._ro_attrs: raise AttributeError(attr) elif attr in self.__dict__: super(Object, self).__setattr__(attr, value) elif isindex(attr): self._values[attr] = value else: self._attrs[attr] = listof(value) self.__dict__['_resolved'] = False def __delattr__(self, attr): if attr in self._reserved_attrs: raise AttributeError(attr) try: if isindex(attr): del self._values[attr] else: del self._attrs[attr] except KeyError: raise AttributeError(attr) self.__dict__['_resolved'] = False def __getitem__(self, attr): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) elif isindex(attr): return self._values[attr] else: return self._attrs[attr] def __setitem__(self, attr, value): value = self.__parse(attr, value) if attr == 'values': self._values[:] = listof(value) elif attr in self._rw_attrs: return super(Object, self).__setattr__('_%s' % attr, value) elif attr in self._ro_attrs: raise KeyError(attr) elif isindex(attr): self._values[attr] = value else: self._attrs[attr] = listof(value) self.__dict__['_resolved'] = False def __delitem__(self, attr): if attr in self._reserved_attrs: raise KeyError(attr) elif isindex(attr): del self._values[attr] else: del self._attrs[attr] self.__dict__['_resolved'] = False # accès spécifique aux valeurs __nonzero__ = lambda self: True def __len__(self): """obtenir le nombre de valeurs""" return len(self._values) def __iter__(self): """obtenir un itérateur sur les valeurs""" return self._values.__iter__() def __reversed__(self): """obtenir la liste des valeurs inversée""" return self._values.__reversed__() def __contains__(self, item): """tester l'existence d'une valeur""" return item in self._values def append(self, value): """ajouter une valeur""" return self._values.append(value) def insert(self, index, value): """insérer une valeur à la position spécifiée""" return self._values.insert(index, value) def extend(self, seq): """étendre la liste des valeurs""" return self._values.extend(seq) # accès spécifique aux attributs def first(self, attr, default=None): """obtenir la première valeur de l'attribut""" if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) if self._attrs.has_key(attr): values = self._attrs[attr] if values: return values[0] return default def get(self, attr, default=None): """obtenir l'attribut sous forme de liste""" if attr in self._reserved_attrs: return listof(getattr(self, '_%s' % attr)) else: return self._attrs.get(attr, default) def has_key(self, attr): """tester l'existence d'un attribut""" if attr in self._reserved_attrs: return True else: return self._attrs.has_key(attr) @property def known_attrs(self): """obtenir une liste triée d'attributs faisant partie du schéma""" return sorted(list(self.ATTRS.keys())) @property def known_rw_attrs(self): """obtenir une liste triée des attributs faisant partie du schéma accessibles en écriture""" return sorted(list(set(self.ATTRS.keys()) - self._ro_attrs)) @property def misc_attrs(self): """obtenir une liste triée d'attributs ne faisant pas partie du schéma""" schema_attrs = set(self.ATTRS.keys()) defined_attrs = set(self._attrs.keys()) return sorted(list(defined_attrs - schema_attrs)) @property def missing_attrs(self): """obtenir une liste triée d'attributs faisant partie du schéma mais non définis""" schema_attrs = set(self.ATTRS.keys()) defined_attrs = set(self._attrs.keys()) return sorted(list(schema_attrs - defined_attrs - self._reserved_attrs)) def update(self, attr, value=None, update_type=ADD_UNIQUE): """mettre à jour l'attribut spécifié si l'attribut n'existe pas, il est créé. sinon, la liste des valeurs de l'attribut est étendue. si value==None, aucune mise à jour n'est effectuée si attr est une instance de dictionnaire, mettre à jour *tous* les attributs spécifiés. update_type est la méthode de mise à jour """ if isinstance(attr, dict): attrs = attr for attr, value in attrs.items(): self.update(attr, value, update_type) return self if attr == 'values': pass elif attr in self._reserved_attrs: raise KeyError(attr) if value is not None: values = listof(self.__parse(attr, value)) if attr == 'values': attr = self._values else: if not self._attrs.has_key(attr): self._attrs[attr] = [] attr = self._attrs[attr] if update_type is ADD_UNIQUE: for value in values: if value not in attr: attr.append(value) elif update_type is ADD: attr.extend(values) elif update_type is REMOVE: for value in values: if value in attr: attr.remove(value) elif update_type is RESET_ADD: attr[:] = values self.__dict__['_resolved'] = False return self def set_defaults(self, attr, value=None, update_type=ADD_UNIQUE): """Mettre à jour l'attribut spécifié s'il n'existe pas si value==None, aucune mise à jour n'est effectuée utiliser attr==None pour mettre à jour l'attribut values si attr est une instance de dictionnaire, mettre à jour *tous* les attributs spécifiés. """ if isinstance(attr, dict): attrs = attr for attr, value in attrs.items(): self.set_defaults(attr, value, update_type) return self if attr == 'values': if not self._values: self.update('values', value, update_type) elif attr in self._reserved_attrs: raise KeyError(attr) elif not self._attrs.has_key(attr): self.update(attr, value, update_type) return self def clone(self): """cloner cet objet""" o = self.__class__(self._oid, self._values, **self._attrs) # XXX cloner aussi les liens return o # gestion des liens def linkto(self, loi, otype=None, **attrs): """lier vers une référence d'un autre objet loi peut être: * une instance de Link * une instance d'Object * un identifiant d'objet, auquel cas otype est requis @return l'instance du lien créé """ if isinstance(loi, Link): create = False link = loi.clone() elif isinstance(loi, Object): otype = loi.otype oid = loi.oid create = True else: if otype is None: raise ValueError('otype is required') oid = loi create = True if create: link = Link(otype, oid, **attrs) else: link.update(attrs) links = self._links if not links.has_key(link.otype): links[otype] = [] links[otype].append(link) self.__dict__['_resolved'] = False return link def get_links(self, otype=None, clone=False): """retourner les liens vers les objets du type spécifié si otype==None, alors retourner tous les liens si clone==True, faire un clone des liens avant de les retourner """ if otype is None: links = [] for otype, tmplinks in self._links.items(): links.extend(tmplinks) else: links = listof(self._links.get(otype, ())) if clone: links = [link.clone() for link in links] return links # catalogue def resolve_basedir(self, basedirs, dirs=False, files=False, filespec=None, dir_attr='dir', file_attr='file', parentdir_attr='parentdir'): """retourner les chemins absolus des fichiers (et/ou répertoires) trouvés dans les répertoires basedirs si les arguments dir_attr, file_attr, parentdir_attr ne sont pas None (ce qui est le cas par défaut), alors l'attribut est mis à jour avec respectivement les répertoires, les fichiers, et les répertoires parent trouvés """ filespecs = listof(filespec, None) result = [] for basedir in basedirs: basedir = path.expanduser(basedir) basedir = path.abspath(basedir) for name in os.listdir(basedir): if filespecs is not None: found = False for filespec in filespecs: if fnmatch(name, filespec): found = True break if not found: continue pf = path.join(basedir, name) if path.isdir(pf) and (dirs or dirs == files): result.append(pf) if dir_attr is not None: self.update(dir_attr, pf) elif path.isfile(pf) and (files or dirs == files): result.append(pf) if file_attr is not None: self.update(file_attr, pf) if parentdir_attr is not None: self.update(parentdir_attr, map(path.dirname, result)) return result def resolve_filespec(self, filespecs, dirs=False, files=False, dir_attr='dir', file_attr='file', parentdir_attr='parentdir'): """retourner les chemins absolus des fichiers (et/ou répertoires) correspondant aux modèles filespecs (qui doivent être de type glob) si les arguments dir_attr, file_attr, parentdir_attr ne sont pas None (ce qui est le cas par défaut), alors l'attribut est mis à jour avec respectivement les répertoires, les fichiers, et les répertoires parent trouvés """ result = [] for filespec in filespecs: filespec = path.expanduser(filespec) for file in glob(filespec): pf = path.abspath(file) if path.isdir(pf) and (dirs or dirs == files): result.append(pf) if dir_attr is not None: self.update(dir_attr, pf) elif path.isfile(pf) and (files or dirs == files): result.append(pf) if file_attr is not None: self.update(file_attr, pf) if parentdir_attr is not None: self.update(parentdir_attr, map(path.dirname, result)) return result def _resolve(self, catalog): """à surcharger dans les classes dérivées""" values = [] search_basedir = self.get('search_basedir', ()) files = 'files' in search_basedir dirs = 'dirs' in search_basedir basedir = self.get('basedir', None) if basedir is not None: values.extend(self.resolve_basedir(basedir, files=files, dirs=dirs)) dirspec = self.get('dirspec', None) if dirspec is not None: values.extend(self.resolve_filespec(dirspec, dirs=True)) filespec = self.get('filespec', None) if filespec is not None: values.extend(self.resolve_filespec(filespec, files=True)) if not self.values: self.values = values def resolve(self, catalog, recursive=True): """normaliser cet objet et compléter les données manquantes. si recursive==True (la valeur par défaut), normaliser aussi les objets liés. @return True si l'objet a été modifié, False si l'objet avait déjà été résolu """ if self._resolved: return False self._resolve(catalog) if recursive: for otype, links in self.links.items(): for link in links: link.resolve(catalog) self.__dict__['_resolved'] = True return True # divers def _dump_idtype(self, indent): print "%s%s:%s" % (indent, self._otype, self._oid) def _dump_values(self, indent): values = self._values if len(values) == 0: pass elif len(values) == 1: print "%s values=%s" % (indent, repr(values[0])) else: print "%s values=(%s)" % (indent, ', '.join(map(repr, values))) def _dump_attrs(self, indent): attrs = self._attrs missing_attrs = self.missing_attrs if attrs or missing_attrs: print "%s attrs:" % indent for name, values in attrs.items(): if len(values) == 1: print "%s %s=%s" % (indent, name, repr(values[0])) else: print "%s %s=(%s)" % (indent, name, ', '.join(map(repr, values))) for name in missing_attrs: print "%s %s=" % (indent, name) def _dump_links(self, indent): if self.links: for ltype, links in self.links.items(): for link in links: link.dump("%s " % indent, '+->') def dump(self, indent=''): """Afficher l'identifiant, le type, les valeurs, les attributs et les liens de cet objet """ self._dump_idtype(indent) self._dump_values(indent) self._dump_attrs(indent) self._dump_links(indent) def __repr__(self): oid = repr(self._oid) values = self._values if values: values = ", %s" % ', '.join(map(repr, values)) else: values = "" attrs = self._attrs if attrs: attrs = ", **%s" % repr(attrs) else: attrs = "" return "%s(%s%s%s)" % (self.__class__.__name__, oid, values, attrs) ################################################################################ # Faits class Fact(object): """Un fait liant deux références d'objets Le fait a le type de l'objet source (propriété `sotype`), son identifiant (propriété `soid`), le verbe décrivant le lien (propriété `verb`), le type de l'objet cible (propriété `totype`), son identifiant (propriété `toid`), et des attributs multivalués (toutes les autres propriétés) """ ATTRS = dict( sotype=None, soid=None, verb=None, totype=None, toid=None, attrs=None, ) _rw_attrs = set(('sotype', 'soid', 'verb', 'totype', 'toid')) _ro_attrs = set(('attrs',)) _reserved_attrs = _rw_attrs | _ro_attrs _sotype = None _soid = None _verb = None _totype = None _toid = None _attrs = None def __init__(self, sotype=None, soid=None, verb=None, totype=None, toid=None, **attrs): if verb.startswith('~'): verb = verb[1:] tmpotype, tmpoid = totype, toid totype, toid = sotype, soid sotype, soid = tmpotype, tmpoid self.__dict__['_sotype'] = sotype self.__dict__['_soid'] = soid self.__dict__['_verb'] = verb self.__dict__['_totype'] = totype self.__dict__['_toid'] = toid self.__dict__['_attrs'] = {} for attr, value in attrs.items(): self.update(attr, value) def __parse(self, attr, value): """obtenir le parser qui permet de s'assurer que value est dans le bon format pour l'attribut attr. """ if isindex(attr): parser = None else: parser = self.ATTRS.get(attr, None) if parser is None: return value elif isseq(value): return flattenseq(map(parser.parse, value)) else: return parser.parse(value) # accès aux attributs def __getattr__(self, attr): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) try: return self._attrs[attr] except KeyError: raise AttributeError(attr) def __setattr__(self, attr, value): value = self.__parse(attr, value) if attr in self._rw_attrs: return super(Link, self).__setattr__('_%s' % attr, value) elif attr in self._ro_attrs: raise AttributeError(attr) else: self._attrs[attr] = listof(value) def __delattr__(self, attr): if attr in self._reserved_attrs: raise AttributeError(attr) try: del self._attrs[attr] except KeyError: raise AttributeError(attr) def __getitem__(self, attr): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) else: return self._attrs[attr] def __setitem__(self, attr, value): value = self.__parse(attr, value) if attr in self._rw_attrs: return super(Link, self).__setattr__('_%s' % attr, value) elif attr in self._ro_attrs: raise KeyError(attr) else: self._attrs[attr] = listof(value) def __delitem__(self, attr): if attr in self._reserved_attrs: raise KeyError(attr) else: del self._attrs[attr] def first(self, attr, default=None): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) if self._attrs.has_key(attr): values = self._attrs[attr] if values: return values[0] return default def get(self, attr, default=None): if attr in self._reserved_attrs: return getattr(self, '_%s' % attr) else: return self._attrs.get(attr, default) def has_key(self, attr): """tester l'existence d'un attribut""" if attr in self._reserved_attrs: return True else: return self._attrs.has_key(attr) @property def known_attrs(self): """obtenir une liste triée d'attributs faisant partie du schéma""" return sorted(list(self.ATTRS.keys())) @property def misc_attrs(self): """obtenir une liste triée d'attributs ne faisant pas partie du schéma""" schema_attrs = set(self.ATTRS.keys()) defined_attrs = set(self._attrs.keys()) return sorted(list(defined_attrs - schema_attrs)) @property def missing_attrs(self): """obtenir une liste triée d'attributs faisant partie du schéma mais non définis""" schema_attrs = set(self.ATTRS.keys()) defined_attrs = set(self._attrs.keys()) return sorted(list(schema_attrs - defined_attrs - self._reserved_attrs)) def update(self, attr, value=None, update_type=ADD_UNIQUE): """mettre à jour l'attribut spécifié si l'attribut n'existe pas, il est créé. sinon, la liste des valeurs de l'attribut est étendue. si value==None, aucune mise à jour n'est effectuée si attr est une instance de dictionnaire, mettre à jour *tous* les attributs spécifiés. update_type est la méthode de mise à jour """ if isinstance(attr, dict): attrs = attr for attr, value in attrs.items(): self.update(attr, value, update_type) return self if attr in self._reserved_attrs: raise KeyError(attr) if value is not None: values = listof(self.__parse(attr, value)) if not self._attrs.has_key(attr): self._attrs[attr] = [] attr = self._attrs[attr] if update_type is ADD_UNIQUE: for value in values: if value not in attr: attr.append(value) elif update_type is ADD: attr.extend(values) elif update_type is REMOVE: for value in values: if value in attr: attr.remove(value) elif update_type is RESET_ADD: attr[:] = values return self def set_defaults(self, attr, value=None, update_type=ADD_UNIQUE): """Mettre à jour l'attribut spécifié s'il n'existe pas si value==None, aucune mise à jour n'est effectuée si attr est une instance de dictionnaire, mettre à jour *tous* les attributs spécifiés s'ils n'existent pas. """ if isinstance(attr, dict): attrs = attr for attr, value in attrs.items(): self.set_defaults(attr, value, update_type) return self if attr in self._reserved_attrs: raise KeyError(attr) if not self._attrs.has_key(attr): self.update(attr, value, update_type) return self def clone(self): """cloner ce lien""" return self.__class__(self._sotype, self._soid, self._verb, self._totype, self._toid, **self._attrs) # catalogue def sresolve(self, catalog, default=_RAISE_EXCEPTION, create=True, resolve=True): return catalog.get(self.sotype, self.soid, default, create, resolve) def tresolve(self, catalog, default=_RAISE_EXCEPTION, create=True, resolve=True): return catalog.get(self.totype, self.toid, default, create, resolve) def resolve(self, catalog, default=_RAISE_EXCEPTION, create=True, resolve=True): """obtenir les objets liés (source, verb, target) """ source = catalog.get(self.sotype, self.soid, default, create, resolve) target = catalog.get(self.totype, self.toid, default, create, resolve) return (source, self.verb, target) # divers def _dump_idtype(self, indent, prefix=None): if prefix is None: prefix = '' else: prefix = "%s " % prefix print "%s%s%s:%s %s %s:%s " % (indent, prefix, self._sotype, self._soid, self._verb, self._totype, self._toid) def _dump_attrs(self, indent): attrs = self._attrs missing_attrs = self.missing_attrs if attrs or missing_attrs: print "%s attrs:" % indent for name, values in attrs.items(): if len(values) == 1: print "%s %s=%s" % (indent, name, repr(values[0])) else: print "%s %s=(%s)" % (indent, name, ', '.join(map(repr, values))) for name in missing_attrs: print "%s %s=" % (indent, name) def dump(self, indent='', prefix=None): """Afficher l'identifiant, le type et les attributs de ce lien """ self._dump_idtype(indent, prefix) self._dump_attrs(indent) ################################################################################ # variables globales catalog = Catalog()