Commits

jason kirtland committed fc5760f

Docs and refinements for Properties.

Comments (0)

Files changed (7)

docs/source/schema/index.rst

    forms
    schema
    traversal
+   properties
    scalars
    datetimes
    dicts

docs/source/schema/properties.rst

+========================
+Annotations & Properties
+========================
+
+.. currentmodule:: flatland.schema.base
+
+Flatland provides two options for annotating schemas and data.
+
+
+Standard Python
+---------------
+
+Element schemas are normal Python classes and can be extended in all
+of the usual ways.  For example, you can add an attribute when
+subclassing:
+
+.. testcode:: pyann
+
+  from flatland import String
+
+  class Textbox(String):
+      tooltip = 'Undefined'
+
+
+Once an attribute has been added to an element class, its value can be
+overridden by further subclassing or, more compactly, with the
+:meth:`~Element.using` schema constructor:
+
+.. testcode:: pyann
+
+  class Password(Textbox):
+      tooltip = 'Enter your password'
+
+
+  Password = Textbox.using(tooltip='Enter your password')
+  assert Password.tooltip == 'Enter your password'
+
+Both are equivalent, and the custom ``tooltip`` will be inherited by
+any subclasses of ``Password``.  Likewise, instances of ``Password``
+will have the attribute as well.
+
+.. testcode:: pyann
+
+  el = Password()
+  assert el.tooltip = 'Enter your password'
+
+And because the :meth:`Element` constructor allows overriding any
+schema attribute by keyword argument, individual element instances can
+be constructed with own values, masking the value provided by their
+class.
+
+.. testcode:: pyann
+
+  password_match = Textbox(tooltip='Enter your password again')
+
+  assert password_match.tooltip == 'Enter your password again'
+
+
+Properties
+----------
+
+Another option for annotation is the :attr:`~Element.properties`
+mapping of element classes and instances.  Unlike class attributes,
+almost any object you like can be used as the key in the mapping.
+
+The unique feature of :attr:`~Element.properties` is data inheritance:
+
+.. testcode:: props
+
+  from flatland import String
+
+  # Textboxes are Strings with tooltips
+  Textbox = String.with_properties(tooltip='Undefined')
+
+  # A Password is a Textbox with a custom tooltip message
+  Password = Textbox.with_properties(tooltip='Enter your password')
+
+  assert Textbox.properties['tooltip'] == 'Undefined'
+  assert Password.properties['tooltip'] == 'Enter your password'
+
+Annotations made on a schema are visible to itself and any subclasses,
+but not to its parents.
+
+.. testcode:: props
+
+  # Add disabled to all Textboxes
+  Textbox.properties['disabled'] = False
+
+  # disabled is inherited from Textbox
+  assert Password.properties['disabled'] is False
+
+  # changes in a subclass do not affect the parent
+  del Password.properties['disabled']
+  assert 'disabled' in Textbox.properties
+
+
+Annotating With Properties
+--------------------------
+
+To create a new schema that includes additional properties, construct
+it with :meth:`~Element.with_properties`:
+
+.. testcode:: props
+
+  Textbox = String.with_properties(tooltip='Undefined')
+
+Or if the schema has already been created, manipulate its
+:attr:`~Element.properties` mapping:
+
+.. testcode:: props
+
+  class Textbox(String):
+     pass
+
+  Textbox.properties['tooltip'] = 'Undefined'
+
+The :attr:`~Element.properties` mapping is implemented as a view over
+the Element schema inheritance hierarchy.  If annotations are added to
+a superclass such as :class:`~flatland.schema.scalars.String`, they
+are visible immediately to all Strings and subclasses.
+
+Private Annotations
+-------------------
+
+To create a schema with completely unrelated properties, not
+inheriting from its superclass at all, declare it with
+:meth:`~Element.using`:
+
+.. testcode:: props
+
+  Alone = Textbox.using(properties={'something': 'else'})
+  assert 'tooltip' not in Alone.properties
+
+Or when subclassing longhand, construct a
+:class:`~flatland.schema.properties.Properties` collection explicitly.
+
+.. testcode:: props
+
+  from flatland import Properties
+
+  class Alone(Textbox):
+     properties = Properties(something='else')
+
+  assert 'tooltip' not in Alone.properties
+
+An instance may also have a private collection of properties.  This
+can be done either at or after construction:
+
+.. testcode:: props
+
+  solo1 = Textbox(properties={'something': 'else'})
+
+  solo2 = Textbox()
+  solo2.properties = {'something': 'else'}
+
+  Textbox.properties['background_color'] = 'red'
+
+  assert 'background_color' not in solo1.properties
+  assert 'background_color' not in solo2.properties
+
+
+..  LocalWords:  pyann tooltip Textbox

flatland/__init__.py

                 'Mapping',
                 'MultiValue',
                 'Number',
+                'Properties',
                 'Ref',
                 'Scalar',
                 'Sequence',

flatland/schema/__init__.py

 from .forms import (
     Form,
     )
+from .properties import (
+    Properties,
+    )

flatland/schema/base.py

         if 'validators' in overrides:
             overrides['validators'] = list(overrides['validators'])
 
+        if 'properties' in overrides:
+            if not isinstance(overrides['properties'], Properties):
+                overrides['properties'] = Properties(overrides['properties'])
+
         for attribute, value in overrides.iteritems():
             # TODO: must make better
             if callable(value):
         return cls
 
     @class_cloner
-    def with_properties(cls, **properties):
+    def with_properties(cls, *iterable, **properties):
         """TODO: doc"""
-        cls.properties.update(properties)
+        simplified = dict(*iterable, **properties)
+        cls.properties.update(simplified)
         return cls
 
     def validate_element(self, element, state, descending):

flatland/schema/properties.py

+from weakref import WeakKeyDictionary
+
 from flatland.util import symbol
 
 
     def __init__(self, *iterable, **initial_set):
         simplified = dict(*iterable, **initial_set)
         self.initial_set = simplified
-        self.map = {}
+        self.map = WeakKeyDictionary()
 
     def __get__(self, instance, cls):
         class_lookup = _TypeLookup(cls, self)

tests/schema/test_properties.py

+from flatland import String
 from flatland.schema.properties import Properties
+
 from nose.tools import assert_raises
 
 
 
     b = Base()
     assert_raises(AttributeError, lambda: b.properties['abc'])
+
+
+def test_dsl():
+    Sub = String.with_properties(abc=123)
+
+    assert 'abc' not in String.properties
+    assert Sub.properties['abc'] == 123
+
+    Disconnected = Sub.using(properties={'def': 456})
+    assert Disconnected.properties['def'] == 456
+    assert 'abc' not in Disconnected.properties
+
+    assert 'def' not in Sub.properties
+    assert 'def' not in String.properties
+
+    Sub.properties['ghi'] = 789
+    assert Disconnected.properties == {'def': 456}
+
+    Disconnected2 = Sub.using(properties=Properties(jkl=123))
+    assert Disconnected2.properties == {'jkl': 123}