Repository: cwacek/python-jsonschema-objects

Issue:
Missing __delattr__
The class `ProtocolBase` misses a specific `__delattr__` implementation. This is required to delete extended properties using `delattr` or `del`. Currently, extended properties can be set with `setattr` but only deleted with array syntax:
```
>>> import python_jsonschema_objects as pjs
>>> builder = pjs.ObjectBuilder({'title': 'Test', 'type': 'object'})                                         
>>> ns = builder.build_classes()
>>> x = ns.Test()
>>> x.foo = 42
>>> x.foo
<Literal<int> 42>
>>> del x.foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: foo
>>> delattr(x, 'foo')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: foo
>>> del x['foo']
```
As far as I can tell, the implementation of `__delattr__` can be identical to the current `__delitem__`, ideally `__delitem__` should simply call `__delattr__` (for symmetry with `__setitem__` and `__setattr__`).

Discussion:

Patch:
diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py
index 65d8660..9ef9dc8 100644
--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -251,16 +251,6 @@ class ProtocolBase(collections.MutableMapping):
         except AttributeError:
             raise KeyError(key)
 
-    def __setitem__(self, key, val):
-        return setattr(self, key, val)
-
-    def __delitem__(self, key):
-        if key in self._extended_properties:
-            del self._extended_properties[key]
-            return
-
-        return delattr(self, key)
-
     def __getattr__(self, name):
         if name in self.__prop_names__:
             raise KeyError(name)
@@ -270,6 +260,24 @@ class ProtocolBase(collections.MutableMapping):
             "{0} is not a valid property of {1}".format(name, self.__class__.__name__)
         )
 
+    def __setitem__(self, key, val):
+        return setattr(self, key, val)
+
+    def __delitem__(self, key):
+        return delattr(self, key)
+
+    def __delattr__(self, name):
+        if name in self._extended_properties:
+            del self._extended_properties[name]
+            return
+
+        if name in self.__prop_names__:
+            prop = getattr(self.__class__, self.__prop_names__[name])
+            prop.__delete__(self)
+            return
+
+        return delattr(self, name)
+
     @classmethod
     def propinfo(cls, propname):
         if propname not in cls.__propinfo__:
diff --git a/python_jsonschema_objects/descriptors.py b/python_jsonschema_objects/descriptors.py
index a53bcbc..fde0ceb 100644
--- a/python_jsonschema_objects/descriptors.py
+++ b/python_jsonschema_objects/descriptors.py
@@ -132,4 +132,4 @@ class AttributeDescriptor(object):
         if prop in obj.__required__:
             raise AttributeError("'%s' is required" % prop)
         else:
-            del obj._properties[prop]
+            obj._properties[prop] = None


Test patch:
diff --git a/test/test_regression_156.py b/test/test_regression_156.py
index 8bb3af4..138bbe0 100644
--- a/test/test_regression_156.py
+++ b/test/test_regression_156.py
@@ -3,7 +3,9 @@ import python_jsonschema_objects as pjo
 
 
 def test_regression_156(markdown_examples):
-    builder = pjo.ObjectBuilder(markdown_examples['MultipleObjects'], resolved=markdown_examples)
+    builder = pjo.ObjectBuilder(
+        markdown_examples["MultipleObjects"], resolved=markdown_examples
+    )
     classes = builder.build_classes(named_only=True)
 
     er = classes.ErrorResponse(message="Danger!", status=99)
diff --git a/test/test_regression_165.py b/test/test_regression_165.py
new file mode 100644
index 0000000..0b5fd97
--- /dev/null
+++ b/test/test_regression_165.py
@@ -0,0 +1,50 @@
+import python_jsonschema_objects as pjs
+import pytest
+
+
+@pytest.fixture
+def testclass():
+    builder = pjs.ObjectBuilder({"title": "Test", "type": "object"})
+    ns = builder.build_classes()
+    return ns.Test()
+
+
+def test_extra_properties_can_be_deleted_with_item_syntax(testclass):
+    testclass.foo = 42
+
+    assert testclass.foo == 42
+    del testclass["foo"]
+
+    # Etestclasstra properties not set should raise AttributeErrors when accessed
+    with pytest.raises(AttributeError):
+        testclass.foo
+
+
+def test_extra_properties_can_be_deleted_with_attribute_syntax(testclass):
+    testclass.foo = 42
+
+    assert testclass.foo == 42
+    del testclass.foo
+
+    # Extra properties not set should raise AttributeErrors when accessed
+    with pytest.raises(AttributeError):
+        testclass.foo
+
+
+def test_extra_properties_can_be_deleted_directly(testclass):
+    testclass.foo = 42
+
+    assert testclass.foo == 42
+    delattr(testclass, "foo")
+
+    # Extra properties not set should raise AttributeErrors when accessed
+    with pytest.raises(AttributeError):
+        testclass.foo
+
+
+def test_real_properties_arent_really_deleted(Person):
+    p = Person(age=20)
+
+    assert p.age == 20
+    del p.age
+    assert p.age == None