[Commits] (pje) Spike: add ``schema.Enumeration`` base type for defining enumerations. Add

commits at osafoundation.org commits at osafoundation.org
Mon Feb 21 12:03:54 PST 2005


Commit by: pje
Modified files:
internal/Spike/src/spike/schema.py 1.4 1.5
internal/Spike/src/spike/schema.txt 1.7 1.8

Log message:
Spike: add ``schema.Enumeration`` base type for defining enumerations.  Add
``displayName`` and ``doc`` options to ``schema.Role`` types, with 
automatic docstring formatting so that entity types read well in pydoc.
Also, add strict attribute checking to ``Role`` types to avoid unintended
misconfiguration or post-initialization changes.


ViewCVS links:
http://cvs.osafoundation.org/index.cgi/internal/Spike/src/spike/schema.py.diff?r1=text&tr1=1.4&r2=text&tr2=1.5
http://cvs.osafoundation.org/index.cgi/internal/Spike/src/spike/schema.txt.diff?r1=text&tr1=1.7&r2=text&tr2=1.8

Index: internal/Spike/src/spike/schema.py
diff -u internal/Spike/src/spike/schema.py:1.4 internal/Spike/src/spike/schema.py:1.5
--- internal/Spike/src/spike/schema.py:1.4	Wed Feb 16 20:27:08 2005
+++ internal/Spike/src/spike/schema.py	Mon Feb 21 12:03:53 2005
@@ -2,7 +2,8 @@
 
 __all__ = [
    'Entity', 'Role', 'Relationship', 'One', 'Many', 'NullSet', 'LoadEvent',
-   'Activator', 'RelationshipClass', 'ActiveDescriptor',
+   'Enumeration', 'Activator', 'RelationshipClass', 'EnumerationClass',
+   'ActiveDescriptor',
 ]
 
 from models import Set
@@ -85,13 +86,15 @@
 
     types = ()
     isReference = False
-    name = owner = _inverse = None
+    name = owner = _inverse = _loadMap = None
+    cardinality = "Role"
 
     def __init__(self,types=(),**kw):
         self.addTypes(types)
         self._loadMap = {}
         for k,v in kw.items():
             setattr(self,k,v)
+        self.setDoc()   # default the doc string
 
     def __setInverse(self,inverse):
         if self._inverse is not inverse:    # No-op if no change
@@ -101,7 +104,7 @@
             try:
                 inverse.inverse = self
             except:
-                self._inverse = None    # roll back the change
+                self._setattr('_inverse',None)  # roll back the change
                 raise
             if self.owner is not None and issubclass(self.owner,Entity):
                 inverse.addTypes(self.owner)
@@ -122,7 +125,7 @@
             raise TypeError("Multiple value types not allowed in one role")
         if refs:
             self.isReference = True
-        self.types += types
+        self._setattr('types',self.types + types)
 
     def activateInClass(self,cls,name):
         """Role was defined/used in class `cls` under name `name`"""
@@ -210,6 +213,58 @@
             return load_role, (self.owner, self.name)
         return object.__reduce_ex__(self,proto)
 
+    def __getDoc(self):
+        return self.__dict__.get('doc')
+
+    def __setDoc(self,val):
+        self.__dict__['doc'] = val
+        self.setDoc()
+
+    doc = property(__getDoc,__setDoc)
+
+    def __getDisplayName(self):
+        return self.__dict__.get('displayName')
+
+    def __setDisplayName(self,val):
+        self.__dict__['displayName'] = val
+        self.setDoc()
+
+    displayName = property(__getDisplayName,__setDisplayName)
+
+    def setDoc(self):
+        doc = self.doc
+        name = self.displayName
+        if not name:
+            name = self.docInfo()
+        else:
+            name = "%s -- %s" % (name,self.docInfo())
+        if not doc:
+            doc = name
+        else:
+            doc = "%s\n\n%s" % (name,doc)
+        self._setattr('__doc__',doc)
+
+    def docInfo(self):
+        return ("%s(%s)" %
+            (self.cardinality,
+                '/'.join([typ.__name__ for typ in self.types]) or '()'
+            )
+        )
+
+    def _setattr(self,attr,value):
+        """Private routine allowing bypass of normal setattr constraints"""
+        super(Role,self).__setattr__(attr,value)
+        
+    def __setattr__(self,attr,value):
+        if not hasattr(type(self),attr):
+            raise TypeError("%r is not a public attribute of %r objects"
+                % (attr,type(self).__name__))
+        old = self.__dict__.get(attr)
+        if old is not None and old<>value:
+            raise TypeError(
+                "Role objects are immutable; can't change %r once set" % attr
+            )
+        self._setattr(attr,value)
 
 NOT_GIVEN = object()
 
@@ -217,6 +272,7 @@
     """Single-valued role attribute"""
 
     default = compute = NOT_GIVEN
+    cardinality = "One"
 
     def newSet(self,ob):
         """Return new default linkset for ``ob``
@@ -256,6 +312,8 @@
 class Many(Role):
     """Multi-valued role attribute"""
 
+    cardinality = "Many"
+
     def __get__(self,ob,typ):
         if ob is None:
             return self
@@ -310,6 +368,71 @@
     __metaclass__ = RelationshipClass
 
 
+class EnumerationClass(Activator):
+    """Metaclass for enumerations"""
+
+    def __init__(cls,name,bases,cdict):
+        super(EnumerationClass,cls).__init__(name,bases,cdict)
+        try:
+            Enumeration
+        except NameError:
+            return  # Enumeration isn't defined yet, so nothing to verify
+
+        if bases<>(Enumeration,):
+            raise TypeError("Enumerations cannot be subclassed")
+
+        d = {}
+        for k,v in cls.iteritems():
+            if v in d:
+                v = unicode(v)
+                raise TypeError(
+                    "Duplicate definitions: %s=enum(%r) and %s=enum(%r)"
+                    % (d[v],v,k,v)
+                )
+            d[v] = k
+
+    def __iter__(cls):
+        for k,v in cls.iteritems():
+            yield v
+
+    def iteritems(cls):
+        """Yield names and values of all enums in this class"""
+        for k in dir(cls):
+            v = getattr(cls,k)
+            if isinstance(v,Enumeration):
+                yield k,v
+
+
+class Enumeration(unicode):
+    """Base class for value types with a fixed set of possible values"""
+
+    __metaclass__ = EnumerationClass
+
+    def __new__(self,*args,**kw):
+        raise TypeError("Enumeration instances cannot be created directly")
+
+    @property
+    def name(self):
+        cls = self.__class__
+        for k,v in cls.iteritems():
+            if v==self:
+                return k
+        raise AssertionError("Invalid instance",unicode(self))
+
+    def __repr__(self):
+        return ".".join([self.__class__.__name__,self.name])
+
+
+class enum(ActiveDescriptor):
+    """schema.enum(displayName) -- define an enumeration value"""
+
+    def __init__(self,displayName):
+        self.displayName = displayName
+
+    def activateInClass(self,cls,name):
+        setattr(cls,name,unicode.__new__(cls,self.displayName))
+
+
 class LoaderWrapper(ActiveDescriptor):
     """Registration wrapper for loader methods declared ``@aRole.loader``"""
 

Index: internal/Spike/src/spike/schema.txt
diff -u internal/Spike/src/spike/schema.txt:1.7 internal/Spike/src/spike/schema.txt:1.8
--- internal/Spike/src/spike/schema.txt:1.7	Wed Feb 16 20:27:08 2005
+++ internal/Spike/src/spike/schema.txt	Mon Feb 21 12:03:53 2005
@@ -566,6 +566,39 @@
         >>> Kind.superkinds.types
         (<class 'Kind'>,)
 
+``displayName`` and ``doc``
+    The name and description of this role, if any.  They will be used to
+    collectively form a ``__doc__`` string, so that ``help()`` is informative
+    for entity types::
+
+        >>> Kind.subkinds.doc = "Sub-kinds of this kind"
+        >>> Kind.superkinds.doc = "Super-kinds of this kind"
+        >>> Kind.name.doc = "This kind's name"
+        >>> Kind.name.displayName = "Kind Name"
+
+        >>> help(Kind)  # doctest: +NORMALIZE_WHITESPACE
+        Help on class Kind ...
+        ...
+        class Kind(spike.schema.Entity)
+         | ...
+         |  Data and other attributes defined here:
+         |
+         |  name = <Role name of <class 'Kind'>>
+         |      Kind Name -- One(str)
+         |      
+         |      This kind's name
+         |  
+         |  subkinds = <Role subkinds of <class 'Kind'>>
+         |      Many(Kind)
+         |      
+         |      Sub-kinds of this kind
+         |  
+         |  superkinds = <Role superkinds of <class 'Kind'>>
+         |      Many(Kind)
+         |      
+         |      Super-kinds of this kind
+         |  ...
+
 
 ``of(ob)``
     Return the linkset for this role and `ob`.  If the role has an ``inverse``,
@@ -671,6 +704,22 @@
         >>> print CalcDemo.alias.getLoader(Dummy())
         <function dummyLoader at ...>
 
+To avoid silent definition errors, ``Role`` classes will not allow setting
+attributes that are not defined by their class::
+
+    >>> r = schema.Role(foo="bar")
+    Traceback (most recent call last):
+    ...
+    TypeError: 'foo' is not a public attribute of 'Role' objects
+
+And, to avoid unintentional alterations, Role attributes can be set only once::
+
+    >>> Kind.superkinds.doc = "testing"
+    Traceback (most recent call last):
+      ...
+    TypeError: Role objects are immutable; can't change 'doc' once set
+
+
 
 Creating Role Subclasses
 ========================
@@ -790,9 +839,9 @@
     Set([Test], type=Kind)
 
 
---------------------------
-Entities and Relationships
---------------------------
+-----------
+Other Types
+-----------
 
 Standalone Relationships
 ========================
@@ -843,6 +892,82 @@
     TypeError: Relationships cannot be subclassed
 
 
+Enumerations
+============
+
+An enumeration is a value type with a fixed set of values, defined using
+the ``schema.Enumeration`` base class, and the ``schema.enum()`` descriptor::
+
+    >>> class Importance(schema.Enumeration):
+    ...     important = schema.enum("Important")
+    ...     normal = schema.enum("Normal")
+    ...     fyi = schema.enum("FYI")
+
+You cannot create enumeration instances directly::
+
+    >>> Importance("x")
+    Traceback (most recent call last):
+    ...
+    TypeError: Enumeration instances cannot be created directly
+
+Instead, you must access instances via the class' attributes::
+
+    >>> Importance.normal
+    Importance.normal
+
+    >>> getattr(Importance,'normal')
+    Importance.normal
+
+Instances hash and compare equal to the string (or unicode) values assigned in
+the class definition, and when converted to a string or unicode, they use that
+same value::
+
+    >>> print Importance.important
+    Important
+
+    >>> unicode(Importance.important)
+    u'Important'
+
+    >>> str(Importance.important)
+    'Important'
+
+    >>> Importance.important == "Important"
+    True
+
+    >>> hash(Importance.important) == hash("Important")
+    True
+
+Note, however, that this means each enumeration value's display name must be
+unique within that enumeration type, and enumeration subclasses are not
+allowed::
+
+    >>> class BadEnum(schema.Enumeration):
+    ...     foo = schema.enum("Spam")
+    ...     bar = schema.enum("Spam")
+    Traceback (most recent call last):
+      ...
+      File "<doctest schema.txt[...]>", line 1, in ?
+        class BadEnum(schema.Enumeration):
+      ...
+    TypeError: Duplicate definitions: bar=enum(u'Spam') and foo=enum(u'Spam')
+
+    >>> class MoreImportant(Importance):
+    ...     super_urgent = schema.enum("Emergency!!!")
+    Traceback (most recent call last):
+    ...
+      File "<doctest schema.txt[...]>", line 1, in ?
+        class MoreImportant(Importance):
+    ...
+    TypeError: Enumerations cannot be subclassed
+
+Finally, note that enumeration types are iterable, and they yield their
+instances when iterated over, so that you can use this information
+programmatically::
+
+    >>> sorted(Importance)
+    [Importance.fyi, Importance.important, Importance.normal]
+
+
 ---------
 Internals
 ---------
@@ -966,7 +1091,7 @@
 
     >>> schema.Role().__reduce_ex__()   # doctest: +NORMALIZE_WHITESPACE
     (<...>, (<class '...schema.Role'>, <type 'object'>, None),
-    {'types': (), '_loadMap': {}})
+    {'__doc__': 'Role(())', 'types': (), '_loadMap': {}})
 
 
 Type Flattening
@@ -1014,9 +1139,6 @@
 
 * Role knows its requiredness, and other metadata (policies?)
 
-* Role.__setattr__ doesn't allow setting unless current value is None/unset, or
-  new value equals old value  (addType should bypass using __set__)
-
 
 Entity Classes
 



More information about the Commits mailing list