Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup and 3.x compatibility #5

Merged
merged 11 commits into from
Jul 30, 2016
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
language: python
python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be nice to have travis build across all versions of python:

diff --git a/.travis.yml b/.travis.yml
index 17306a6..116b0a4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,13 @@
 language: python
 python:
+  - "2.6"
   - "2.7"
+  - "3.2"
+  - "3.3"
+  - "3.4"
+  - "3.5"
+  - "3.5-dev" # 3.5 development branch
+  - "nightly" # currently points to 3.6-dev
 install:
   - pip install codecov pep8
   - python setup.py install

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should support 3.2 because very few people use it and 3.2 compatibility is more complicated: the u'unicode string' was removed in 3.0 and only readded in 3.3.

Given that travis relies on volunteers for computing resources, I don't think we should have a build for every version. I think 2.7, 3.3, and nightly should be enough, and maybe 2.6 if that is necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should support 3.2 because very few people use it and 3.2 compatibility is more complicated: the u'unicode string' was removed in 3.0 and only readded in 3.3.

Ok, that sounds reasonable. Why don't you update the README with a list of supported Python versions so that it's clear.

Given that travis relies on volunteers for computing resources

I wouldn't worry about that too much. Millions of projects use Travis, and us using a few more builds is not going to make a difference.

I think 2.7, 3.3, and nightly should be enough, and maybe 2.6 if that is necessary.

Given my above reasoning, how about 2.6, 3.3, 3.4, 3.5, 3.5-dev, and nightly? I think 2.6 might not have codecov. If that's the case, omit the codecov step. I'm not too keen on 2.6, so if it's causing lots of problems, don't worry about it. But the other versions I listed I think we should try to support if possible.

- "3.5"
- "3.5-dev"
- "nightly"
install:
- pip install -r requirements.txt
- pip install codecov pep8
- python setup.py install
script:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Lispify [![Build Status](https://travis-ci.org/infolab-csail/lispify.svg?branch=master)](https://travis-ci.org/infolab-csail/lispify) [![codecov](https://codecov.io/gh/infolab-csail/lispify/branch/master/graph/badge.svg)](https://codecov.io/gh/infolab-csail/lispify)
Lispify converts Python objects into Lisp-like encoded strings that are interpretable in Common Lisp.
Lispify converts Python objects into Lisp-like encoded strings that are interpretable in Common Lisp. This library requires Python 2.6, 2.7, or 3.3+.

## Releases

Expand All @@ -12,3 +12,4 @@ Version numbers `MAJOR.MINOR.PATCH` should follow a system inspired by [semantic
1. PATCH version when you make backwards-compatible bug fixes.

Create a [release](https://help.github.com/articles/creating-releases/) on Github for each new version.
>>>>>>> master
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete

54 changes: 25 additions & 29 deletions lispify/lispify.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@
from __future__ import absolute_import

from numbers import Number
from sys import version_info
import warnings

from lispify.util import subclasses, camel_case_to_lisp_name

from future.utils import python_2_unicode_compatible
from past.builtins import basestring

# python 3 compatibility functions
def string_types():
return str if version_info.major > 2 else basestring

# For fully deterministic lisp types use this priority.
MIN_PRIORITY = 0
Expand All @@ -31,7 +29,6 @@ def mid_priority():
"""
Just count. Later classes override previous ones.
"""

for i in range(MIN_PRIORITY + 1, MAX_PRIORITY):
yield i

Expand All @@ -42,6 +39,7 @@ def mid_priority():
MID_PRIORITY = mid_priority()


@python_2_unicode_compatible
class LispType(object):

"""
Expand Down Expand Up @@ -69,7 +67,7 @@ def __init__(self, val):
if self.should_parse(val):
self.val = self.parse_val(val)
else:
raise ValueError(u'failed to lispify {}'.format(val))
raise ValueError(u'failed to lispify {0}'.format(val))

def should_parse(self, val):
"""
Expand All @@ -88,17 +86,11 @@ def __repr__(self):
repr(self.original_val))

def __str__(self):
if version_info.major >= 3:
return self.__unicode__()
else:
return unicode(self).encode('utf-8')

def __unicode__(self):
# we do u'{}'.format twice so bad val_str implementation still works
return u'{}'.format(self.val_str())
return u'{0}'.format(self.val_str())

def val_str(self):
return u'{}'.format(self.val)
return u'{0}'.format(self.val)

def __nonzero__(self):
return True
Expand All @@ -115,10 +107,15 @@ def __hash__(self):


class LispString(LispType):

"""
Encodes a Lisp string.
"""

priority = next(MID_PRIORITY)

def should_parse(self, val):
return isinstance(val, string_types())
return isinstance(val, basestring)

def val_str(self):
v = self.val.replace('"', '\\"') # escape double quotes
Expand All @@ -129,27 +126,27 @@ def val_str(self):
class LispList(LispType):

"""
This is coordinates and other things like that
Encodes iterable objects (except for strings) as a list.
"""

priority = next(MID_PRIORITY)
literal = True

def should_parse(self, val):
return hasattr(val, '__iter__') and not isinstance(val, string_types())
return hasattr(val, '__iter__') and not isinstance(val, basestring)

def erepr(self, v):
if isinstance(v, LispType):
return v.val_str()

try:
return u'{}'.format(lispify(v))
return u'{0}'.format(lispify(v))
except NotImplementedError:
warnings.warn('Lispifying an unknown type!')
return repr(v)

def val_str(self):
return u'({})'.format(' '.join([self.erepr(v) for v in self.val]))
return u'({0})'.format(' '.join([self.erepr(v) for v in self.val]))

def __contains__(self, val):
return (self.erepr(val) in map(self.erepr, self.val))
Expand All @@ -168,7 +165,7 @@ def should_parse(self, val):

def val_str(self):
pairs = sorted(self.val.items())
return u'({})'.format(self._plist(pairs))
return u'({0})'.format(self._plist(pairs))

def _plist(self, pairs):
"""
Expand All @@ -178,11 +175,11 @@ def _plist(self, pairs):

def _kv_pair(self, k, v):
if k is None:
return u':{}'.format(v)
elif isinstance(k, string_types()):
return u':{} {}'.format(k, v)
return u':{0}'.format(v)
elif isinstance(k, basestring):
return u':{0} {1}'.format(k, v)
else:
raise ValueError('Key {} must be None or string'.format(k))
raise ValueError('Key {0} must be None or string'.format(k))

def _paren_content_iter(self, pairs):
for k, v in pairs:
Expand Down Expand Up @@ -246,7 +243,7 @@ def val_str(self):
keys=self._plist(kw.items())
)
else:
return u'(:error %s)'.format(symbol)
return u'(:error {symbol})'.format(symbol=symbol)

def __nonzero__(self):
"""
Expand Down Expand Up @@ -275,7 +272,7 @@ class LispKeyword(_LispLiteral):
"""

def should_parse(self, val):
return (isinstance(val, string_types()) and
return (isinstance(val, basestring) and
val.startswith(':') and
' ' not in val)

Expand Down Expand Up @@ -312,14 +309,13 @@ def should_parse(self, val):
return isinstance(val, Number)


LISP_TYPES = subclasses(LispType, instantiate=False)
LISP_TYPES = subclasses(LispType)


def lispify(obj):
"""
Return a Lisp-like encoded string.
"""

if isinstance(obj, LispType):
return obj

Expand All @@ -329,7 +325,7 @@ def lispify(obj):
except ValueError:
pass

raise NotImplementedError('Implement LispType for val: {} or provide '
raise NotImplementedError('Implement LispType for val: {0} or provide '
'fallback error.'.format(obj))

__all__ = ['lispify']
11 changes: 3 additions & 8 deletions lispify/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
import re


def subclasses(cls, instantiate=True, **kw):
def subclasses(cls):
"""
A list of instances of subclasses of cls. Instantiate wit kw and
return just the classes with instantiate=False.
A list of subclasses of cls.
"""

lcls = cls.__subclasses__()
Expand All @@ -16,11 +15,7 @@ def subclasses(cls, instantiate=True, **kw):

clss = sorted(rcls, key=lambda c: c.priority, reverse=True)

if not instantiate:
return [C for C in clss
if not C.__name__.startswith("_")]

return [C(**kw) for C in clss
return [C for C in clss
if not C.__name__.startswith("_")]


Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
future==0.15.2
unittest2==1.1.0
72 changes: 42 additions & 30 deletions tests/test_lispify.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,53 +18,57 @@
from lispify.lispify import lispify


def to_lisp_string(obj):
return str(lispify(obj))


class TestLispify(unittest.TestCase):

def test_string(self):
self.assertEqual(str(lispify("foo")),
self.assertEqual(to_lisp_string("foo"),
'"foo"')

def test_string_escaped(self):
self.assertEqual(str(lispify('foo \\ "bar"')),
self.assertEqual(to_lisp_string('foo \\ "bar"'),
'"foo \\ \\"bar\\""')

@unittest.skipIf(version_info.major == 2, 'python 3 string behavior')
def test_encodings_py3(self):
self.assertEqual(str(lispify(u"föø ” ")),
u'"föø ” "')

@unittest.skipUnless(version_info.major == 2, 'python 2 string behavior')
def test_encodings_py2(self):
self.assertEqual(str(lispify(u"föø ” ")),
u'"föø ” "'.encode('utf-8'))
self.assertEqual(unicode(lispify(u"föø ” ")),
u'"föø ” "')
def test_encodings(self):
if version_info < (3,):
# python 2 behavior
self.assertEqual(to_lisp_string(u"föø ” "),
u'"föø ” "'.encode('utf-8'))
self.assertEqual(unicode(lispify(u"föø ” ")),
u'"föø ” "')
else:
# python 3 behavior
self.assertEqual(to_lisp_string(u"föø ” "),
u'"föø ” "')

def test_list(self):
l = ['wikipedia-class1', 'wikipedia-class2']
self.assertEqual(str(lispify(l)),
self.assertEqual(to_lisp_string(l),
'("wikipedia-class1" "wikipedia-class2")')

def test_nested_list(self):
l = [[0, 'foo'], [1, '"bar"']]
self.assertEqual(str(lispify(l)),
self.assertEqual(to_lisp_string(l),
'((0 "foo") (1 "\\"bar\\""))')

def test_nested_tuple(self):
# this test needs to be separated from nested list because tuples
# are used in string formatting (see #222 for an example of failure)
t = ('foo', ('bar', 'baz'))
self.assertEqual(str(lispify(t)),
self.assertEqual(to_lisp_string(t),
'("foo" ("bar" "baz"))')

def test_double_nested_list(self):
l = [[0, ['v0', 'foo']], [1, ['v1', 'bar']]]
self.assertEqual(str(lispify(l)),
self.assertEqual(to_lisp_string(l),
'((0 ("v0" "foo")) (1 ("v1" "bar")))')

def test_list_of_dict(self):
l = [{'foo': 'bar'}, {'foo': 'baz'}]
self.assertEqual(str(lispify(l)),
self.assertEqual(to_lisp_string(l),
'((:foo "bar") (:foo "baz"))')

def test_list_contains(self):
Expand All @@ -74,53 +78,61 @@ def test_list_contains(self):

def test_date_simple(self):
date = {'yyyymmdd': '00000808'}
self.assertEqual(str(lispify(date)),
self.assertEqual(to_lisp_string(date),
'(:yyyymmdd 00000808)')

def test_date_multiple_keys(self):
date = {'yyyymmdd': '19491001', 'html': 'Oct 1, 1949'}
self.assertEqual(str(lispify(date)),
self.assertEqual(to_lisp_string(date),
'(:html "Oct 1, 1949" :yyyymmdd 19491001)')

def test_bool(self):
self.assertEqual(str(lispify(True)), 't')
self.assertEqual(str(lispify(False)), 'nil')
self.assertEqual(to_lisp_string(True), 't')
self.assertEqual(to_lisp_string(False), 'nil')

def test_keyword(self):
self.assertEqual(str(lispify(':feminine')),
self.assertEqual(to_lisp_string(':feminine'),
':feminine')

def test_string_not_keyword(self):
self.assertEqual(str(lispify(':not a keyword')),
self.assertEqual(to_lisp_string(':not a keyword'),
'":not a keyword"')

def test_dict(self):
self.assertEqual(str(lispify({'a': 1, 'b': "foo"})),
self.assertEqual(to_lisp_string({'a': 1, 'b': "foo"}),
'(:a 1 :b "foo")')

def test_dict_with_escaped_string(self):
self.assertEqual(str(lispify({'a': 1, 'b': '"foo"'})),
self.assertEqual(to_lisp_string({'a': 1, 'b': '"foo"'}),
'(:a 1 :b "\\"foo\\"")')

def test_dict_with_list(self):
self.assertEqual(str(lispify({'a': 1, 'b': ['foo', 'bar']})),
self.assertEqual(to_lisp_string({'a': 1, 'b': ['foo', 'bar']}),
'(:a 1 :b ("foo" "bar"))')

def test_error_from_exception(self):
err = ValueError('Wrong thing')
self.assertEqual(str(lispify(err)),
self.assertEqual(to_lisp_string(err),
'(:error value-error :message "Wrong thing")')

err = NotImplementedError()
self.assertEqual(to_lisp_string(err),
'(:error not-implemented-error)')

def test_none(self):
self.assertEqual(str(lispify(None)), 'nil')
self.assertEqual(to_lisp_string(None), 'nil')

def test_number(self):
self.assertEqual(str(lispify(5)), '5')
self.assertEqual(to_lisp_string(5), '5')

def test_lispified(self):
val = {"hello": "world"}
self.assertEqual(lispify(val), lispify(lispify(val)))

def test_partially_lispified(self):
val = [lispify(1), lispify(2)]
self.assertEqual(to_lisp_string(val), '(1 2)')

@unittest.expectedFailure
def test_unimplemented(self):
lispify(lambda x: x)
Expand Down