Repository: cwacek/python-jsonschema-objects

Issue:
Literal Types no longer can be directly operated on
With the new support for default values for literals (which is great), doing stuff like:
   `object.value + 1` or `object.name.startswith('John')` no longer works.

```
from python_jsonschema_objects.classbuilder import MakeLiteral
# expect 10, get an error that 'test' + 'test' is not defined
MakeLiteral('test', 'integer', 5) + MakeLiteral('test', 'integer', 5) 
# expect "hello world", get an error that 'test' + 'test' is not defined
MakeLiteral('test', 'string', 'hello ') + MakeLiteral('test', 'integer', ' world!') 
```

There's a few quick fix: use str(), float(), int() to get the _value or just do ._value everywhere.

However, it'd be great if we just dispatch the calls to the underlying literals.

i have a [gist](https://gist.github.com/blackhathedgehog/c88e1b85352e531fe6a98b0b13a96f2e) that is a quick-and-dirty implementation. i think that an improved implementation might actually run `MakeLiteral` and return a value that is wrapped (and validated). 


Discussion:

Patch:
diff --git a/.codeclimate.yml b/.codeclimate.yml
new file mode 100644
index 0000000..30e4b1d
--- /dev/null
+++ b/.codeclimate.yml
@@ -0,0 +1,9 @@
+engines:
+  pep8:
+    enabled: true
+    checks:
+      E128:
+        enabled: false
+exclude_paths:
+  - versioneer.py
+
diff --git a/.hound.yml b/.hound.yml
deleted file mode 100644
index 7b9ecc1..0000000
--- a/.hound.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-flake8:
-  enabled: true
diff --git a/.travis.yml b/.travis.yml
index b4be679..6fc198a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,9 +2,23 @@ language: python
 python:
 - '2.7'
 - '3.5'
+env:
+  global:
+    - CC_TEST_REPORTER_ID=dc84306013e5ab029d115d8c2f9b27f23ee83b8d73c2dc35fed00954769fc76b
+
+before_script:
+  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
+  - chmod +x ./cc-test-reporter
+  - ./cc-test-reporter before-build
+
 install:
 - pip install tox-travis tox
+
 script: tox
+
+after_script:
+  - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
+
 deploy:
   distributions: "sdist bdist_wheel"
   provider: pypi
diff --git a/python_jsonschema_objects/classbuilder.py b/python_jsonschema_objects/classbuilder.py
index ed55b99..4c0eb06 100644
--- a/python_jsonschema_objects/classbuilder.py
+++ b/python_jsonschema_objects/classbuilder.py
@@ -1,6 +1,7 @@
 import python_jsonschema_objects.util as util
 import python_jsonschema_objects.validators as validators
 import python_jsonschema_objects.pattern_properties as pattern_properties
+from python_jsonschema_objects.literals import LiteralValue
 
 import collections
 import itertools
@@ -16,11 +17,11 @@ logger = logging.getLogger(__name__)
 logger.addHandler(logging.NullHandler())
 
 
-
 # Long is no longer a thing in python3.x
 if sys.version_info > (3,):
   long = int
 
+
 class ProtocolBase(collections.MutableMapping):
     """ An instance of a class generated from the provided
     schema. All properties will be validated according to
@@ -75,16 +76,16 @@ class ProtocolBase(collections.MutableMapping):
 
     def __str__(self):
         inverter = dict((v, k) for k,v in six.iteritems(self.__prop_names__))
-        props = ["%s" % (inverter.get(k, k),) for k, v in
+        props = sorted(["%s" % (inverter.get(k, k),) for k, v in
                  itertools.chain(six.iteritems(self._properties),
-                                 six.iteritems(self._extended_properties))]
+                                 six.iteritems(self._extended_properties))])
         return "<%s attributes: %s>" % (self.__class__.__name__, ", ".join(props))
 
     def __repr__(self):
         inverter = dict((v, k) for k,v in six.iteritems(self.__prop_names__))
-        props = ["%s=%s" % (inverter.get(k, k), str(v)) for k, v in
+        props = sorted(["%s=%s" % (inverter.get(k, k), str(v)) for k, v in
                  itertools.chain(six.iteritems(self._properties),
-                                 six.iteritems(self._extended_properties))]
+                                 six.iteritems(self._extended_properties))])
         return "<%s %s>" % (
             self.__class__.__name__,
             " ".join(props)
@@ -237,9 +238,9 @@ class ProtocolBase(collections.MutableMapping):
             return {}
         return cls.__propinfo__[propname]
 
-    def serialize(self):
+    def serialize(self, **opts):
         self.validate()
-        enc = util.ProtocolJSONEncoder()
+        enc = util.ProtocolJSONEncoder(**opts)
         return enc.encode(self)
 
     def validate(self):
@@ -281,18 +282,6 @@ class ProtocolBase(collections.MutableMapping):
         return True
 
 
-def MakeLiteral(name, typ, value, **properties):
-    properties.update({'type': typ})
-    klass = type(str(name), tuple((LiteralValue,)), {
-        '__propinfo__': {
-            '__literal__': properties,
-            '__default__': properties.get('default')
-        }
-    })
-
-    return klass(value)
-
-
 class TypeProxy(object):
 
     def __init__(self, types):
@@ -322,80 +311,6 @@ class TypeProxy(object):
             )
 
 
-class LiteralValue(object):
-  """Docstring for LiteralValue """
-
-  isLiteralClass = True
-
-  def __init__(self, value, typ=None):
-      """@todo: to be defined
-
-      :value: @todo
-
-      """
-      if isinstance(value, LiteralValue):
-          self._value = value._value
-      else:
-          self._value = value
-
-      if self._value is None and self.default() is not None:
-          self._value = self.default()
-
-      self.validate()
-
-  def as_dict(self):
-      return self.for_json()
-
-  def for_json(self):
-      return self._value
-
-  @classmethod
-  def default(cls):
-      return cls.__propinfo__.get('__default__')
-
-  @classmethod
-  def propinfo(cls, propname):
-      if propname not in cls.__propinfo__:
-          return {}
-      return cls.__propinfo__[propname]
-
-  def serialize(self):
-      self.validate()
-      enc = util.ProtocolJSONEncoder()
-      return enc.encode(self)
-
-  def __repr__(self):
-      return "<%s %s>" % (
-          self.__class__.__name__,
-          str(self._value)
-      )
-
-  def __str__(self):
-      return str(self._value)
-
-  def validate(self):
-      info = self.propinfo('__literal__')
-
-      # this duplicates logic in validators.ArrayValidator.check_items; unify it.
-      for param, paramval in sorted(six.iteritems(info), key=lambda x: x[0].lower() != 'type'):
-          validator = validators.registry(param)
-          if validator is not None:
-              validator(paramval, self._value, info)
-
-  def __eq__(self, other):
-      return self._value == other
-
-  def __hash__(self):
-      return hash(self._value)
-
-  def __lt__(self, other):
-      return self._value < other
-
-  def __int__(self):
-    return int(self._value)
-
-  def __float__(self):
-    return float(self._value)
 
 
 class ClassBuilder(object):
diff --git a/python_jsonschema_objects/examples/README.md b/python_jsonschema_objects/examples/README.md
index 700c7b6..31f59fc 100644
--- a/python_jsonschema_objects/examples/README.md
+++ b/python_jsonschema_objects/examples/README.md
@@ -55,29 +55,36 @@ here that the schema above has been loaded in a variable called
 
 ``` python
 >>> import python_jsonschema_objects as pjs
->>> builder = pjs.ObjectBuilder(schema)
+>>> builder = pjs.ObjectBuilder(examples['Example Schema'])
 >>> ns = builder.build_classes()
 >>> Person = ns.ExampleSchema
 >>> james = Person(firstName="James", lastName="Bond")
 >>> james.lastName
-u'Bond'
+<Literal<str> Bond>
+>>> james.lastName == "Bond"
+True
 >>> james
-<example_schema lastName=Bond age=None firstName=James>
+<example_schema address=None age=None deceased=None dogs=None firstName=James gender=None lastName=Bond>
+
 ```
 
 Validations will also be applied as the object is manipulated.
 
 ``` python
->>> james.age = -2
-python_jsonschema_objects.validators.ValidationError: -2 was less
-or equal to than 0
+>>> james.age = -2  # doctest: +IGNORE_EXCEPTION_DETAIL
+Traceback (most recent call last):
+    ...
+ValidationError: -2 is less than 0
+
 ```
 
-The object can be serialized out to JSON:
+The object can be serialized out to JSON. Options are passed
+through to the standard library JSONEncoder object.
 
 ``` python
->>> james.serialize()
-'{"lastName": "Bond", "age": null, "firstName": "James"}'
+>>> james.serialize(sort_keys=True)
+'{"firstName": "James", "lastName": "Bond"}'
+
 ```
 
 ## Why
@@ -101,7 +108,36 @@ with validation, directly from an input JSON schema. These
 classes can seamlessly encode back and forth to JSON valid
 according to the schema.
 
-## Other Features
+## Fully Functional Literals
+
+Literal values are wrapped when constructed to support validation
+and other schema-related operations. However, you can still use
+them just as you would other literals.
+
+``` python
+>>> import python_jsonschema_objects as pjs
+>>> builder = pjs.ObjectBuilder(examples['Example Schema'])
+>>> ns = builder.build_classes()
+>>> Person = ns.ExampleSchema
+>>> james = Person(firstName="James", lastName="Bond")
+>>> str(james.lastName)
+'Bond'
+>>> james.lastName += "ing"
+>>> str(james.lastName)
+'Bonding'
+>>> james.age = 4
+>>> james.age - 1
+3
+>>> 3 + james.age
+7
+>>> james.lastName / 4
+Traceback (most recent call last):
+    ...
+TypeError: unsupported operand type(s) for /: 'str' and 'int'
+
+```
+
+## Resolving Directly from Memory
 
 The ObjectBuilder can be passed a dictionary specifying
 'memory' schemas when instantiated. This will allow it to
@@ -221,11 +257,11 @@ The schema and code example below show how this works.
 ```
 
 ``` python
->>> builder = pjs.ObjectBuilder('multiple_objects.json')
+>>> builder = pjs.ObjectBuilder(examples["MultipleObjects"])
 >>> classes = builder.build_classes()
->>> print(dir(classes))
-[u'ErrorResponse', 'Local', 'Message', u'Multipleobjects',
-'Status', 'Version', u'VersionGetResponse']
+>>> [str(x) for x in dir(classes)]
+['ErrorResponse', 'Local', 'Message', 'Multipleobjects', 'Status', 'Version', 'VersionGetResponse']
+
 ```
 
 ## Installation
diff --git a/python_jsonschema_objects/literals.py b/python_jsonschema_objects/literals.py
new file mode 100644
index 0000000..9b0c02d
--- /dev/null
+++ b/python_jsonschema_objects/literals.py
@@ -0,0 +1,146 @@
+from python_jsonschema_objects import util
+from python_jsonschema_objects import validators
+import functools
+import logging
+import six
+import operator
+
+
+def MakeLiteral(name, typ, value, **properties):
+    properties.update({'type': typ})
+    klass = type(str(name), tuple((LiteralValue,)), {
+        '__propinfo__': {
+            '__literal__': properties,
+            '__default__': properties.get('default')
+        }
+    })
+
+    return klass(value)
+
+
+class LiteralValue(object):
+  """Docstring for LiteralValue """
+
+  isLiteralClass = True
+
+  def __init__(self, value, typ=None):
+      """@todo: to be defined
+
+      :value: @todo
+
+      """
+      if isinstance(value, LiteralValue):
+          self._value = value._value
+      else:
+          self._value = value
+
+      if self._value is None and self.default() is not None:
+          self._value = self.default()
+
+      self.validate()
+
+  def as_dict(self):
+      return self.for_json()
+
+  def for_json(self):
+      return self._value
+
+  @classmethod
+  def default(cls):
+      return cls.__propinfo__.get('__default__')
+
+  @classmethod
+  def propinfo(cls, propname):
+      if propname not in cls.__propinfo__:
+          return {}
+      return cls.__propinfo__[propname]
+
+  def serialize(self):
+      self.validate()
+      enc = util.ProtocolJSONEncoder()
+      return enc.encode(self)
+
+  def __repr__(self):
+      return "<Literal<%s> %s>" % (
+          self._value.__class__.__name__,
+          str(self._value)
+      )
+
+  def __str__(self):
+      if isinstance(self._value, six.string_types):
+        return self._value
+      return str(self._value)
+
+  def validate(self):
+      info = self.propinfo('__literal__')
+
+      # TODO: this duplicates logic in validators.ArrayValidator.check_items; unify it.
+      for param, paramval in sorted(six.iteritems(info),
+                                    key=lambda x: x[0].lower() != 'type'):
+          validator = validators.registry(param)
+          if validator is not None:
+              validator(paramval, self._value, info)
+
+  def __eq__(self, other):
+      return self._value == other
+
+  def __hash__(self):
+      return hash(self._value)
+
+  def __lt__(self, other):
+      return self._value < other
+
+  def __int__(self):
+    return int(self._value)
+
+  def __float__(self):
+    return float(self._value)
+
+
+EXCLUDED_OPERATORS = set(
+    util.CLASS_ATTRS +
+    util.NEWCLASS_ATTRS +
+    ["__name__",
+     "__setattr__",
+     "__getattr__",
+     "__dict__",
+     "__matmul__",
+     "__imatmul__",
+     ]
+)
+
+
+def dispatch_to_value(fn):
+  def wrapper(self, other):
+    return fn(self._value, other)
+    pass
+  return wrapper
+
+
+""" This attaches all the literal operators to LiteralValue
+ except for the reverse ones."""
+for op in dir(operator):
+    if op.startswith("__") and op not in EXCLUDED_OPERATORS:
+      opfn = getattr(operator, op)
+      setattr(LiteralValue, op, dispatch_to_value(opfn))
+
+
+""" We also have to patch the reverse operators,
+which aren't conveniently defined anywhere """
+LiteralValue.__radd__ = lambda self, other: other + self._value
+LiteralValue.__rsub__ = lambda self, other: other - self._value
+LiteralValue.__rmul__ = lambda self, other: other * self._value
+LiteralValue.__rtruediv__ = lambda self, other: other / self._value
+LiteralValue.__rfloordiv__ = lambda self, other: other // self._value
+LiteralValue.__rmod__ = lambda self, other: other % self._value
+LiteralValue.__rdivmod__ = lambda self, other: divmod(other, self._value)
+LiteralValue.__rpow__ = lambda self, other, modulo=None: pow(
+    other, self._value, modulo)
+LiteralValue.__rlshift__ = lambda self, other: other << self._value
+LiteralValue.__rrshift__ = lambda self, other: other >> self._value
+LiteralValue.__rand__ = lambda self, other: other & self._value
+LiteralValue.__rxor__ = lambda self, other: other ^ self._value
+LiteralValue.__ror__ = lambda self, other: other | self._value
+
+
+
diff --git a/python_jsonschema_objects/pattern_properties.py b/python_jsonschema_objects/pattern_properties.py
index c2617c2..b15f104 100644
--- a/python_jsonschema_objects/pattern_properties.py
+++ b/python_jsonschema_objects/pattern_properties.py
@@ -3,6 +3,8 @@ import six
 import re
 import python_jsonschema_objects.validators as validators
 import python_jsonschema_objects.util as util
+from python_jsonschema_objects.literals import MakeLiteral
+
 import collections
 
 import logging
@@ -97,7 +99,7 @@ class ExtensibleValidator(object):
                        in validators.SCHEMA_TYPE_MAPPING
                        if t is not None and isinstance(val, t)]
             valtype = valtype[0]
-            return cb.MakeLiteral(name, valtype, val)
+            return MakeLiteral(name, valtype, val)
 
         elif isinstance(self._additional_type, type):
             return self._make_type(self._additional_type, val)
diff --git a/python_jsonschema_objects/util.py b/python_jsonschema_objects/util.py
index ad8f7da..5b6533f 100644
--- a/python_jsonschema_objects/util.py
+++ b/python_jsonschema_objects/util.py
@@ -124,6 +124,7 @@ from collections import Mapping, Sequence
 class _Dummy:
     pass
 CLASS_ATTRS = dir(_Dummy)
+NEWCLASS_ATTRS = dir(object)
 del _Dummy
 
 
diff --git a/tox.ini b/tox.ini
index 0207557..dff2994 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,7 @@ envlist = py27, py35
 
 [testenv]
 ;install_command = pip install {opts} {packages}
-commands = coverage run {envbindir}/py.test {posargs}
+commands = coverage run {envbindir}/py.test --doctest-glob='python_jsonschema_objects/*.md'  {posargs} 
            coverage html --omit=*test* --include=*python_jsonschema_objects*
 deps =
   .


Test patch:
diff --git a/test/conftest.py b/conftest.py
similarity index 66%
rename from test/conftest.py
rename to conftest.py
index 664a782..4f0e1d8 100644
--- a/test/conftest.py
+++ b/conftest.py
@@ -4,15 +4,20 @@ import json
 import pkg_resources
 import os
 import python_jsonschema_objects as pjs
+import python_jsonschema_objects.markdown_support
 
 
 @pytest.fixture
 def markdown_examples():
         md = pkg_resources.resource_filename('python_jsonschema_objects.examples', 'README.md')
-        examples = pjs.markdown_support.extract_code_blocks(md)
+        examples = python_jsonschema_objects.markdown_support.extract_code_blocks(md)
         examples = {json.loads(v)['title']: json.loads(v) for v in examples['schema']}
         return examples
 
+@pytest.fixture(autouse=True)
+def inject_examples(doctest_namespace, markdown_examples):
+    doctest_namespace['examples'] = markdown_examples
+
 
 @pytest.fixture
 def Person(markdown_examples):
diff --git a/test/test_regression_8.py b/test/test_regression_8.py
index 19a08c2..41d5cc2 100644
--- a/test/test_regression_8.py
+++ b/test/test_regression_8.py
@@ -48,7 +48,7 @@ def test_array_elements_compare_to_types(test_instance):
     assert test
 
 def test_repr_shows_property_values(test_instance):
-    expected = "<example/arrayProp_<anonymous_field> these>"
+    expected = "<Literal<str> these>"
     assert repr(test_instance.arrayProp[0]) == expected
 
 def test_str_shows_just_strings(test_instance):