diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..654f8b9 --- /dev/null +++ b/README.txt @@ -0,0 +1,5 @@ +Doxygen documentation generation : + doxygen + +Dynamic code generation : + python3 scripts/refreshdyn.py examples/em_test.pickle OUTPUTFILE.py diff --git a/lodel/editorial_model/components.py b/lodel/editorial_model/components.py index d0cc803..1a72d7d 100644 --- a/lodel/editorial_model/components.py +++ b/lodel/editorial_model/components.py @@ -11,6 +11,7 @@ from lodel.editorial_model.exceptions import * ## @brief Abstract class to represent editorial model components # @see EmClass EmField +# @todo forbid '.' in uid class EmComponent(object): ## @brief Instanciate an EmComponent @@ -43,8 +44,6 @@ class EmComponent(object): ## @brief Handles editorial model objects classes -# -# @note The inheritance system allow child classes to overwrite parents EmField. But it's maybe not a good idea class EmClass(EmComponent): ## @brief Instanciate a new EmClass @@ -166,12 +165,14 @@ class EmField(EmComponent): def __init__(self, uid, data_handler, display_name = None, help_text = None, group = None, **handler_kwargs): from lodel.leapi.datahandlers.base_classes import DataHandler super().__init__(uid, display_name, help_text, group) + ## @brief The data handler name self.data_handler_name = data_handler + ## @brief The data handler class self.data_handler_cls = DataHandler.from_name(data_handler) - #if 'data_handler_kwargs' in handler_kwargs: - # handler_kwargs = handler_kwargs['data_handler_kwargs'] - self.data_handler_options = handler_kwargs + ## @brief The data handler instance associated with this EmField self.data_handler_instance = self.data_handler_cls(**handler_kwargs) + ## @brief Stores data handler instanciation options + self.data_handler_options = handler_kwargs ## @brief Stores the emclass that contains this field (set by EmClass.add_field() method) self._emclass = None diff --git a/lodel/editorial_model/model.py b/lodel/editorial_model/model.py index 0d441be..28ac776 100644 --- a/lodel/editorial_model/model.py +++ b/lodel/editorial_model/model.py @@ -39,6 +39,27 @@ class EditorialModel(object): return self.__elt_getter(self.__groups, uid) except KeyError: raise EditorialModelException("EmGroup not found : '%s'" % uid) + + ## @brief EmField getter + # @param uid str : An EmField uid represented by "CLASSUID.FIELDUID" + # @return Fals or an EmField instance + # + # @todo delete it, useless... + def field(self, uid = None): + spl = uid.split('.') + if len(spl) != 2: + raise ValueError("Malformed EmField identifier : '%s'" % uid) + cls_uid = spl[0] + field_uid = spl[1] + try: + emclass = self.classes(cls_uid) + except KeyError: + return False + try: + return emclass.fields(field_uid) + except KeyError: + pass + return False ## @brief Add a class to the editorial model # @param emclass EmClass : the EmClass instance to add diff --git a/lodel/leapi/datahandlers/base_classes.py b/lodel/leapi/datahandlers/base_classes.py index e8e3b3a..6b9e643 100644 --- a/lodel/leapi/datahandlers/base_classes.py +++ b/lodel/leapi/datahandlers/base_classes.py @@ -34,15 +34,23 @@ class DataHandler(object): # designed globally and immutable # @param **args # @throw NotImplementedError if it is instanciated directly - def __init__(self, internal=False, immutable=False, primary_key = False, **args): + def __init__(self, **kwargs): if self.__class__ == DataHandler: raise NotImplementedError("Abstract class") - self.primary_key = primary_key - self.internal = internal # Check this value ? - self.immutable = bool(immutable) + self.__arguments = kwargs - for argname, argval in args.items(): + self.nullable = True + self.uniq = False + self.immutable = False + self.primary_key = False + if 'defaults' in kwargs: + self.default, error = self.check_data_value(kwargs['default']) + if error: + raise error + del(args['default']) + + for argname, argval in kwargs.items(): setattr(self, argname, argval) ## Fieldtype name @@ -50,6 +58,10 @@ class DataHandler(object): def name(cls): return cls.__module__.split('.')[-1] + @classmethod + def is_reference(cls): + return issubclass(cls, Reference) + def is_primary_key(self): return self.primary_key @@ -61,10 +73,12 @@ class DataHandler(object): ## @brief calls the data_field defined _check_data_value() method # @return tuple (value, error|None) def check_data_value(self, value): - return self._check_data_value(value) + if value is None: + if not self.nullable: + return None, TypeError("'None' value but field is not nullable") - def _check_data_value(self, value): - return value, None + return None, None + return self._check_data_value(value) ## @brief checks if this class can override the given data handler # @param data_handler DataHandler @@ -159,37 +173,7 @@ class DataHandler(object): ## @brief Base class for datas data handler (by opposition with references) class DataField(DataHandler): - - ## @brief Instanciates a new fieldtype - # @param nullable bool : is None allowed as value ? - # @param uniq bool : Indicates if a field should handle a uniq value - # @param primary bool : If true the field is a primary key - # @param internal str|False: if False, that field is not internal. Other values cans be "autosql" or "internal" - # @param **kwargs : Other arguments - # @throw NotImplementedError if called from bad class - def __init__(self, internal=False, nullable=True, uniq=False, primary=False, **kwargs): - if self.__class__ == DataField: - raise NotImplementedError("Abstract class") - - super().__init__(internal, **kwargs) - - self.nullable = nullable - self.uniq = uniq - self.primary = primary - if 'defaults' in kwargs: - self.default, error = self.check_data_value(kwargs['default']) - if error: - raise error - del(args['default']) - - def check_data_value(self, value): - if value is None: - if not self.nullable: - return None, TypeError("'None' value but field is not nullable") - - return None, None - return super().check_data_value(value) - + pass ## @brief Abstract class for all references # @@ -199,11 +183,30 @@ class Reference(DataHandler): ## @brief Instanciation # @param allowed_classes list | None : list of allowed em classes if None no restriction + # @param back_reference tuple | None : tuple containing (EmClass name, EmField name) # @param internal bool : if False, the field is not internal # @param **kwargs : other arguments - def __init__(self, allowed_classes = None, internal=False, **kwargs): + def __init__(self, allowed_classes = None, back_reference = None, internal=False, **kwargs): self.__allowed_classes = None if allowed_classes is None else set(allowed_classes) + if back_reference is not None: + if len(back_reference) != 2: + raise ValueError("A tuple (classname, fieldname) expected but got '%s'" % back_reference) + self.__back_reference = back_reference super().__init__(internal=internal, **kwargs) + + @property + def back_reference(self): + return self.__back_reference + + ## @brief Set the back reference for this field. + # + # This method is designed to be called from LeObject child classes + # at dyncode load. LeObject dynamic childs classes are the objects that are + # able to fetch python classes from name. + def _set_back_reference(self, back_reference = None): + + pass + ## @brief Check value # @param value * diff --git a/lodel/leapi/datahandlers/datas.py b/lodel/leapi/datahandlers/datas.py index 763bfd5..cccdd29 100644 --- a/lodel/leapi/datahandlers/datas.py +++ b/lodel/leapi/datahandlers/datas.py @@ -65,10 +65,9 @@ class UniqID(Integer): ## @brief A uid field # @param **kwargs - def __init__(self, is_id_class, **kwargs): - self._is_id_class = is_id_class + def __init__(self, **kwargs): kwargs['internal'] = 'automatic' - super(self.__class__, self).__init__(is_id_class=is_id_class, **kwargs) + super(self.__class__, self).__init__(primary_key = True, **kwargs) def _check_data_value(self, value): return value, None diff --git a/lodel/leapi/datahandlers/datas_base.py b/lodel/leapi/datahandlers/datas_base.py index c52b206..33f9d91 100644 --- a/lodel/leapi/datahandlers/datas_base.py +++ b/lodel/leapi/datahandlers/datas_base.py @@ -30,7 +30,7 @@ class Integer(DataField): base_type = 'int' def __init__(self, **kwargs): - super().__init__(base_type='int', **kwargs) + super().__init__( **kwargs) def _check_data_value(self, value): error = None diff --git a/lodel/leapi/lefactory.py b/lodel/leapi/lefactory.py index 9da4067..8639f5f 100644 --- a/lodel/leapi/lefactory.py +++ b/lodel/leapi/lefactory.py @@ -9,18 +9,30 @@ from lodel.leapi.datahandlers.base_classes import DataHandler # @param model lodel.editorial_model.model.EditorialModel def dyncode_from_em(model): + # Generation of LeObject child classes code cls_code, modules, bootstrap_instr = generate_classes(model) + # Completing bootstrap with back_reference bootstraping + for leoname in [ LeObject.name2objname(emcls.uid) for emcls in get_classes(model) ]: + bootstrap_instr += """ +{leobject}._backref_init() +""".format(leobject = leoname) + bootstrap_instr += """ +del(LeObject._set__fields) +del(LeObject._backref_init) +""" + + # Header imports = """from lodel.leapi.leobject import LeObject from lodel.leapi.datahandlers.base_classes import DataField """ for module in modules: imports += "import %s\n" % module - + + # formating all components of output res_code = """#-*- coding: utf-8 -*- {imports} {classes} {bootstrap_instr} -del(LeObject._set__fields) """.format( imports = imports, classes = cls_code, @@ -49,6 +61,10 @@ def emclass_sorted_by_deps(emclass_list): return 0 return sorted(emclass_list, key = functools.cmp_to_key(emclass_deps_cmp)) +## @brief Returns a list of EmClass that will be represented as LeObject child classes +def get_classes(model): + return [ cls for cls in emclass_sorted_by_deps(model.classes()) if not cls.pure_abstract ] + ## @brief Given an EmField returns the data_handler constructor suitable for dynamic code def data_handler_constructor(emfield): #dh_module_name = DataHandler.module_name(emfield.data_handler_name)+'.DataHandler' @@ -84,7 +100,7 @@ def generate_classes(model): imports = list() bootstrap = "" # Generating field list for LeObjects generated from EmClass - for em_class in [ cls for cls in emclass_sorted_by_deps(model.classes()) if not cls.pure_abstract ]: + for em_class in get_classes(model): uid = list() # List of fieldnames that are part of the EmClass primary key parents = list() # List of parents EmClass # Determine pk @@ -101,9 +117,9 @@ def generate_classes(model): # Dynamic code generation for LeObject childs classes em_cls_code = """ class {clsname}({parents}): - __abstract = {abstract} - __fields = None - __uid = {uid_list} + _abstract = {abstract} + _fields = None + _uid = {uid_list} """.format( clsname = LeObject.name2objname(em_class.uid), diff --git a/lodel/leapi/leobject.py b/lodel/leapi/leobject.py index 803d850..95fec2f 100644 --- a/lodel/leapi/leobject.py +++ b/lodel/leapi/leobject.py @@ -1,5 +1,7 @@ #-*- coding: utf-8 -*- +import importlib + class LeApiErrors(Exception): ## @brief Instanciate a new exceptions handling multiple exceptions # @param msg str : Exception message @@ -64,26 +66,26 @@ class LeObjectValues(object): class LeObject(object): ## @brief boolean that tells if an object is abtract or not - __abtract = None + _abstract = None ## @brief A dict that stores DataHandler instances indexed by field name - __fields = None + _fields = None ## @brief A tuple of fieldname (or a uniq fieldname) representing uid - __uid = None + _uid = None ## @brief Construct an object representing an Editorial component # @note Can be considered as EmClass instance def __init__(self, **kwargs): - if self.__abstract: + if self._abstract: raise NotImplementedError("%s is abstract, you cannot instanciate it." % self.__class__.__name__ ) ## @brief A dict that stores fieldvalues indexed by fieldname - self.__datas = { fname:None for fname in self.__fields } + self.__datas = { fname:None for fname in self._fields } ## @brief Store a list of initianilized fields when instanciation not complete else store True self.__initialized = list() ## @brief Datas accessor. Instance of @ref LeObjectValues self.d = LeObjectValues(self.fieldnames, self.set_data, self.data) # Checks that uid is given - for uid_name in self.__uid: + for uid_name in self._uid: if uid_name not in kwargs: raise AttributeError("Cannot instanciate a LeObject without it's identifier") self.__datas[uid_name] = kwargs[uid_name] @@ -95,7 +97,7 @@ class LeObject(object): err_list = list() for fieldname, fieldval in kwargs.items(): if fieldname not in allowed_fieldnames: - if fieldname in self.__fields: + if fieldname in self._fields: err_list.append( AttributeError("Value given for internal field : '%s'" % fieldname) ) @@ -123,9 +125,9 @@ class LeObject(object): @classmethod def fieldnames(cls, include_ro = False): if not include_ro: - return [ fname for fname in self.__fields if not self.__fields[fname].is_internal() ] + return [ fname for fname in self._fields if not self._fields[fname].is_internal() ] else: - return list(self.__fields.keys()) + return list(self._fields.keys()) @classmethod def name2objname(cls, name): @@ -136,10 +138,43 @@ class LeObject(object): # @return A data handler instance @classmethod def data_handler(cls, fieldname): - if not fieldname in cls.__fields: + if not fieldname in cls._fields: raise NameError("No field named '%s' in %s" % (fieldname, cls.__name__)) - return cls.__fields[fieldname] - + return cls._fields[fieldname] + + ## @brief Return a LeObject child class from a name + # @warning This method has to be called from dynamically generated LeObjects + # @param leobject_name str : LeObject name + # @return A LeObject child class + # @throw NameError if invalid name given + @classmethod + def name2class(cls, leobject_name): + if cls.__module__ == 'lodel.leapi.leobject': + raise NotImplementedError("Abstract method") + mod = importlib.import_module(cls.__module__) + try: + return getattr(mod, leobject_name) + except AttributeError: + raise NameError("No LeObject named '%s'" % leobject_name) + + ## @brief Method designed to "bootstrap" fields by settings their back references + # + # @note called once at dyncode load + # @note doesn't do any consistency checks, it assume that checks has been done EM side + # @warning after dyncode load this method is deleted + @classmethod + def _backref_init(cls): + for fname,field in cls._fields.items(): + if field.is_reference() and field.back_reference is not None: + cls_name, field_name = field.back_reference + bref_leobject = cls.name2class(cls.name2objname(cls_name)) + if field_name not in bref_leobject._fields: + raise NameError("LeObject %s doesn't have a field named '%s'" % (cls_name, field_name)) + field.set_backreference(bref_leobject._fields[field_name]) + + @classmethod + def is_abstract(cls): + return cls._abstract ## @brief Read only access to all datas # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance @@ -148,7 +183,7 @@ class LeObject(object): # @throw RuntimeError if the field is not initialized yet # @throw NameError if name is not an existing field name def data(self, field_name): - if field_name not in self.__fields.keys(): + if field_name not in self._fields.keys(): raise NameError("No such field in %s : %s" % (self.__class__.__name__, name)) if not self.initialized and name not in self.__initialized: raise RuntimeError("The field %s is not initialized yet (and have no value)" % name) @@ -163,7 +198,7 @@ class LeObject(object): # @throw AttributeError if the field is not writtable def set_data(self, fname, fval): if field_name not in self.fieldnames(include_ro = False): - if field_name not in self.__fields.keys(): + if field_name not in self._fields.keys(): raise NameError("No such field in %s : %s" % (self.__class__.__name__, name)) else: raise AttributeError("The field %s is read only" % fname) @@ -182,7 +217,7 @@ class LeObject(object): else: # Doing value check on modified field # We skip full validation here because the LeObject is not fully initialized yet - val, err = self.__fields[fname].check_data_value(fval) + val, err = self._fields[fname].check_data_value(fval) if isinstance(err, Exception): #Revert change to be in valid state del(self.__datas[fname]) @@ -196,7 +231,7 @@ class LeObject(object): # Check the list of initialized fields and set __initialized to True if all fields initialized def __set_initialized(self): if isinstance(self.__initialized, list): - expected_fields = self.fieldnames(include_ro = False) + self.__uid + expected_fields = self.fieldnames(include_ro = False) + self._uid if set(expected_fields) == set(self.__initialized): self.__initialized = True @@ -209,7 +244,7 @@ class LeObject(object): if self.__initialized is True: # Data value check for fname in self.fieldnames(include_ro = False): - val, err = self.__fields[fname].check_data_value(self.__datas[fname]) + val, err = self._fields[fname].check_data_value(self.__datas[fname]) if err is not None: err_list[fname] = err else: @@ -218,7 +253,7 @@ class LeObject(object): if len(err_list) == 0: for fname in self.fieldnames(include_ro = True): try: - field = self.__fields[fname] + field = self._fields[fname] self.__datas[fname] = fields.construct_data( self, fname, self.__datas, @@ -229,14 +264,14 @@ class LeObject(object): # Datas consistency check if len(err_list) == 0: for fname in self.fieldnames(include_ro = True): - field = self.__fields[fname] + field = self._fields[fname] ret = field.check_data_consistency(self, fname, self.__datas) if isinstance(ret, Exception): err_list[fname] = ret else: # Data value check for initialized datas for fname in self.__initialized: - val, err = self.__fields[fname].check_data_value(self.__datas[fname]) + val, err = self._fields[fname].check_data_value(self.__datas[fname]) if err is not None: err_list[fname] = err else: @@ -249,13 +284,13 @@ class LeObject(object): ## @brief Temporary method to set private fields attribute at dynamic code generation # - # This method is used in the generated dynamic code to set the __fields attribute + # This method is used in the generated dynamic code to set the _fields attribute # at the end of the dyncode parse - # @warning This method is deleted once the dynamic code is parsed + # @warning This method is deleted once the dynamic code loaded # @param field_list list : list of EmField instance @classmethod def _set__fields(cls, field_list): - cls.__fields = field_list + cls._fields = field_list