Skip to content
This repository has been archived by the owner on Jun 3, 2023. It is now read-only.

Commit

Permalink
Implemented transit_types.Boolean plus true,false.
Browse files Browse the repository at this point in the history
  • Loading branch information
benkamphaus committed Sep 19, 2014
2 parents 68e2b47 + d25e7df commit e5ae544
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 15 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ writer.write(TaggedValue("ints", [1,2,3]))

### Python's bool and int

All caveats of the Python language apply to decoding Transit data.

In Python, bools are subclasses of int (that is, `True` is actually `1`).

```python
Expand All @@ -108,7 +106,7 @@ In Python, bools are subclasses of int (that is, `True` is actually `1`).
True
```

This only becomes problematic when decoding a map that contains bool and
This becomes problematic when decoding a map that contains bool and
int keys. The bool keys may be overridden (ie: you'll only see the int key),
and the value will be one of any possible bool/int keyed value.

Expand All @@ -117,6 +115,14 @@ and the value will be one of any possible bool/int keyed value.
{1: 'World'}
```

To counter this problem, the latest version of Transit Python introduces a
Boolean type with singleton (by convention of use) instances of "true" and
"false." A Boolean can be converted to a native Python bool with bool(x) where
x is the "true" or "false" instance. Logical evaluation works correctly with
Booleans (that is, they override the __nonzero__ method and correctly evaluate
as true and false in simple logical evaluation), but uses of a Boolean as an
integer will fail.

### Default type mapping

|Transit type|Write accepts|Read returns|
Expand Down
10 changes: 5 additions & 5 deletions tests/exemplars_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# then import transit stuff
from transit.reader import Reader, JsonUnmarshaler, MsgPackUnmarshaler
from transit.writer import Writer
from transit.transit_types import Keyword, Symbol, URI, frozendict, TaggedValue, Link
from transit.transit_types import Keyword, Symbol, URI, frozendict, TaggedValue, Link, true, false
from StringIO import StringIO
from transit.helpers import mapcat
from helpers import ints_centered_on, hash_of_size, array_of_symbools
Expand Down Expand Up @@ -96,7 +96,7 @@ def assertEqual(self, val, data):
globals()["test_" + name + "_json"] = ExemplarTest

ARRAY_SIMPLE = (1, 2, 3)
ARRAY_MIXED = (0, 1, 2.0, True, False, 'five', Keyword("six"), Symbol("seven"), '~eight', None)
ARRAY_MIXED = (0, 1, 2.0, true, false, 'five', Keyword("six"), Symbol("seven"), '~eight', None)
ARRAY_NESTED = (ARRAY_SIMPLE, ARRAY_MIXED)
SMALL_STRINGS = ("", "a", "ab", "abc", "abcd", "abcde", "abcdef")
POWERS_OF_TWO = tuple(map(lambda x: pow(2, x), range(66)))
Expand Down Expand Up @@ -131,14 +131,14 @@ def assertEqual(self, val, data):

MAP_MIXED = frozendict({Keyword("a"): 1,
Keyword("b"): u"a string",
Keyword("c"): True})
Keyword("c"): true})

MAP_NESTED = frozendict({Keyword("simple"): MAP_SIMPLE,
Keyword("mixed"): MAP_MIXED})

exemplar("nil", None)
exemplar("true", True)
exemplar("false", False)
exemplar("true", true)
exemplar("false", false)
exemplar("zero", 0)
exemplar("one", 1)
exemplar("one_string", "hello")
Expand Down
36 changes: 34 additions & 2 deletions tests/regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from transit.reader import Reader
from transit.writer import Writer
from transit.transit_types import Symbol, frozendict
from transit.transit_types import Symbol, frozendict, true, false
from StringIO import StringIO

class RegressionBaseTest(unittest.TestCase):
Expand All @@ -37,12 +37,44 @@ def test_roundtrip(self):

globals()["test_" + name + "_json"] = RegressionTest

regression("cache_consistency", ({"Problem?":True},
regression("cache_consistency", ({"Problem?":true},
Symbol("Here"),
Symbol("Here")))
regression("one_pair_frozendict", frozendict({"a":1}))
regression("json_int_max", (2**53+100, 2**63+100))
regression("newline_in_string", "a\nb")

class BooleanTest(unittest.TestCase):
"""Even though we're roundtripping transit_types.true and
transit_types.false now, make sure we can still write Python bools.
Additionally, make sure we can still do basic logical evaluation on transit
Boolean values.
"""
def test_write_bool(self):
for protocol in ("json", "json-verbose", "msgpack"):
io = StringIO()
w = Writer(io, protocol)
w.write((True, False))
r = Reader(protocol)
io.seek(0)
out_data = r.read(io)
assert out_data[0] == true
assert out_data[1] == false

def test_basic_eval(self):
assert true
assert not false

def test_or(self):
assert true or false
assert not (false or false)
assert true or true

def test_and(self):
assert not (true and false)
assert true and true
assert not (false and false)

if __name__ == '__main__':
unittest.main()
6 changes: 4 additions & 2 deletions transit/decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from helpers import pairs
import read_handlers as rh
from rolling_cache import RollingCache, is_cacheable, is_cache_key
from transit_types import true, false

class Tag(object):
def __init__(self, tag):
Expand Down Expand Up @@ -88,8 +89,9 @@ def _decode(self, node, cache, as_map_key):
return self.decode_list(node, cache, as_map_key)
elif tp is str:
return self.decode_string(unicode(node, "utf-8"), cache, as_map_key)
else:
return node
elif tp is bool:
return true if node else false
return node

def decode_list(self, node, cache, as_map_key):
"""Special case decodes map-as-array.
Expand Down
2 changes: 1 addition & 1 deletion transit/read_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def from_rep(v):
class BooleanHandler(object):
@staticmethod
def from_rep(x):
return x == "t"
return transit_types.true if x == "t" else transit_types.false

class IntHandler(object):
@staticmethod
Expand Down
26 changes: 26 additions & 0 deletions transit/transit_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,29 @@ def as_map(self):
@property
def as_array(self):
return [self.href, self.rel, self.name, self.render, self.prompt]

class Boolean(object):
"""To allow a separate t/f that won't hash as 1/0. Don't call directly,
instead use true and false as singleton objects. Can use with type check.
Note that the Booleans are for preserving hash/set bools that duplicate 1/0
and not designed for use in Python outside of logical evaluation (don't treat
as an int, they're not). You can get a Python bool using bool(x)
where x is a true or false Boolean.
"""
def __init__(self, name):
self.v = True if name == "true" else False
self.name = name

def __nonzero__(self):
return self.v

def __repr__(self):
return self.name

def __str__(self):
return self.name

# lowercase rep matches java/clojure
false = Boolean("false")
true = Boolean("true")
5 changes: 3 additions & 2 deletions transit/write_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from constants import *
from class_hash import ClassDict
from transit_types import Keyword, Symbol, URI, frozendict, TaggedValue, Link
from transit_types import Keyword, Symbol, URI, frozendict, TaggedValue, Link, Boolean
import uuid
import datetime, time
from dateutil import tz
Expand Down Expand Up @@ -103,7 +103,7 @@ def tag(_):
return '?'
@staticmethod
def rep(b):
return b
return bool(b)
@staticmethod
def string_rep(b):
return 't' if b else 'f'
Expand Down Expand Up @@ -258,6 +258,7 @@ def __init__(self):
super(WriteHandler, self).__init__()
self[type(None)] = NoneHandler
self[bool] = BooleanHandler
self[Boolean] = BooleanHandler
self[str] = StringHandler
self[unicode] = StringHandler
self[list] = ArrayHandler
Expand Down

0 comments on commit e5ae544

Please sign in to comment.