Skip to content

Commit 0efe5e2

Browse files
Introduce NonCons type and use Python ABCs to register classes
1 parent d9599ae commit 0efe5e2

File tree

5 files changed

+119
-61
lines changed

5 files changed

+119
-61
lines changed

README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ The `cons` package attempts to emulate the semantics of Lisp/Scheme's `cons` as
1919

2020
>>> cons(1, [2, 3])
2121
[1, 2, 3]
22-
23-
>>> cons(1, "a string")
24-
ConsPair(1 'a string')
2522
```
2623

27-
According to `cons`, `None` corresponds to the empty built-in `list`:
24+
In general, `cons` is designed to work with `collections.abc.Sequence` types.
25+
26+
According to the `cons` package, `None` corresponds to the empty built-in `list`, as `nil` does in some Lisps:
2827
```python
2928
>>> cons(1, None)
3029
[1]
@@ -44,17 +43,26 @@ ConsError: Not a cons pair
4443

4544
```
4645

46+
By default, `str` types are not considered cons-pairs, although they are sequences:
47+
```python
48+
>>> cons("a", "string")
49+
ConsPair('a' 'a string')
50+
```
51+
52+
This setting can be overridden and other types can be similarly excluded from consideration by registering classes with the `abc`-based classes `MaybeCons` and `NonCons`.
53+
54+
4755
Features
4856
===========
4957

50-
* Support for the standard Python ordered collection types: i.e. `list`, `tuple`, `Iterator`, `OrderedDict`.
58+
* Built-in support for the standard Python ordered sequence types: i.e. `list`, `tuple`, `Iterator`, `OrderedDict`.
5159
```python
5260
>>> from collections import OrderedDict
5361
>>> cons(('a', 1), OrderedDict())
5462
OrderedDict([('a', 1)])
5563

5664
```
57-
* Existing `cons` behavior is easy to change and new collections are straightforward to add through [`multipledispatch`][md].
65+
* Existing `cons` behavior can be changed and support for new collections can be added through the generic functions `cons.core._car` and `cons.core._cdr`.
5866
* Built-in support for [`unification`][un].
5967
```python
6068
>>> from unification import unify, reify, var
@@ -78,5 +86,4 @@ pip install cons
7886

7987

8088
[cons]: https://en.wikipedia.org/wiki/Cons
81-
[md]: https://github.com/mrocklin/multipledispatch
8289
[un]: https://github.com/mrocklin/unification

cons/core.py

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
from abc import ABCMeta, ABC, abstractmethod
12
from functools import reduce
23
from operator import length_hint
3-
from collections import OrderedDict
4+
from collections import OrderedDict, UserString
45
from itertools import chain, islice
5-
from collections.abc import Iterator, Sequence, ItemsView
6+
from collections.abc import Iterator, Sequence, ItemsView, ByteString
67

78
from multipledispatch import dispatch
89

@@ -23,15 +24,15 @@ class ConsError(ValueError):
2324
pass
2425

2526

26-
class ConsType(type):
27+
class ConsType(ABCMeta):
2728
def __instancecheck__(self, o):
2829
return (
2930
issubclass(type(o), (ConsPair, MaybeCons))
3031
and length_hint(o, 0) > 0
3132
)
3233

3334

34-
class ConsNullType(type):
35+
class ConsNullType(ABCMeta):
3536
def __instancecheck__(self, o):
3637
if o is None:
3738
return True
@@ -63,7 +64,9 @@ class ConsNull(metaclass=ConsNullType):
6364
is returned, and it signifies the uncertainty of the negative assertion.
6465
"""
6566

66-
pass
67+
@abstractmethod
68+
def __init__(self):
69+
pass
6770

6871

6972
class ConsPair(metaclass=ConsType):
@@ -137,7 +140,7 @@ def __eq__(self, other):
137140
)
138141

139142
def __repr__(self):
140-
return "{}({} {})".format(
143+
return "{}({}, {})".format(
141144
self.__class__.__name__, repr(self.car), repr(self.cdr)
142145
)
143146

@@ -148,15 +151,15 @@ def __str__(self):
148151
cons = ConsPair
149152

150153

151-
class MaybeConsType(type):
152-
_ignored_types = (object, type(None), ConsPair, str)
153-
154+
class MaybeConsType(ABCMeta):
154155
def __subclasscheck__(self, o):
155-
return o not in self._ignored_types and any(
156-
issubclass(o, d)
157-
for d in cdr.funcs.keys()
158-
if d not in self._ignored_types
159-
)
156+
157+
if issubclass(o, tuple(_cdr.funcs.keys())) and not issubclass(
158+
o, NonCons
159+
):
160+
return True
161+
162+
return False
160163

161164

162165
class MaybeCons(metaclass=MaybeConsType):
@@ -172,21 +175,47 @@ class MaybeCons(metaclass=MaybeConsType):
172175
functions.
173176
"""
174177

175-
pass
178+
@abstractmethod
179+
def __init__(self):
180+
pass
181+
182+
183+
class NonCons(ABC):
184+
"""A class (and its subclasses) that is *not* considered a cons.
185+
186+
This type/class can be used as a means of excluding certain types from
187+
consideration as a cons pair (i.e. via `NotCons.register`).
188+
"""
189+
190+
@abstractmethod
191+
def __init__(self):
192+
pass
193+
194+
195+
for t in (type(None), str, set, UserString, ByteString):
196+
NonCons.register(t)
176197

177198

178-
@dispatch((type(None), str))
179199
def car(z):
180-
raise ConsError("Not a cons pair")
200+
if issubclass(type(z), ConsPair):
201+
return z.car
202+
203+
try:
204+
return _car(z)
205+
except NotImplementedError:
206+
raise ConsError("Not a cons pair")
181207

182208

183-
@car.register(ConsPair)
184-
def car_ConsPair(z):
185-
return z.car
209+
@dispatch(Sequence)
210+
def _car(z):
211+
try:
212+
return first(z)
213+
except StopIteration:
214+
raise ConsError("Not a cons pair")
186215

187216

188-
@car.register(Iterator)
189-
def car_Iterator(z):
217+
@_car.register(Iterator)
218+
def _car_Iterator(z):
190219
"""Return the first element in the given iterator.
191220
192221
Warning: `car` necessarily draws from the iterator, and we can't do
@@ -201,48 +230,40 @@ def car_Iterator(z):
201230
raise ConsError("Not a cons pair")
202231

203232

204-
@car.register(Sequence)
205-
def car_Sequence(z):
206-
try:
207-
return first(z)
208-
except StopIteration:
209-
raise ConsError("Not a cons pair")
210-
211-
212-
@car.register(OrderedDict)
213-
def car_OrderedDict(z):
233+
@_car.register(OrderedDict)
234+
def _car_OrderedDict(z):
214235
if len(z) == 0:
215236
raise ConsError("Not a cons pair")
216237

217238
return first(z.items())
218239

219240

220-
@dispatch((type(None), str))
221241
def cdr(z):
222-
raise ConsError("Not a cons pair")
223-
224-
225-
@cdr.register(ConsPair)
226-
def cdr_ConsPair(z):
227-
return z.cdr
242+
if issubclass(type(z), ConsPair):
243+
return z.cdr
228244

229-
230-
@cdr.register(Iterator)
231-
def cdr_Iterator(z):
232-
if length_hint(z, 1) == 0:
245+
try:
246+
return _cdr(z)
247+
except NotImplementedError:
233248
raise ConsError("Not a cons pair")
234-
return rest(z)
235249

236250

237-
@cdr.register(Sequence)
238-
def cdr_Sequence(z):
251+
@dispatch(Sequence)
252+
def _cdr(z):
239253
if len(z) == 0:
240254
raise ConsError("Not a cons pair")
241255
return type(z)(rest(z))
242256

243257

244-
@cdr.register(OrderedDict)
245-
def cdr_OrderedDict(z):
258+
@_cdr.register(Iterator)
259+
def _cdr_Iterator(z):
260+
if length_hint(z, 1) == 0:
261+
raise ConsError("Not a cons pair")
262+
return rest(z)
263+
264+
265+
@_cdr.register(OrderedDict)
266+
def _cdr_OrderedDict(z):
246267
if len(z) == 0:
247268
raise ConsError("Not a cons pair")
248269
return cdr(list(z.items()))

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
[tool:pytest]
2+
filterwarnings =
3+
ignore:Using or importing the ABCs from 'collections':DeprecationWarning:unification.core
24
python_files=test*.py
35
testpaths=tests

setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44

55
setup(
66
name="cons",
7-
version="0.1.2",
7+
version="0.1.3",
88
install_requires=[
99
'toolz',
10-
'multipledispatch',
1110
'unification'
1211
],
1312
packages=find_packages(exclude=['tests']),

tests/test_cons.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from unification import unify, reify, var
99

1010
from cons import cons, car, cdr
11-
from cons.core import ConsPair, MaybeCons, ConsNull, rest, ConsError
11+
from cons.core import ConsPair, MaybeCons, ConsNull, rest, ConsError, NonCons
1212

1313

1414
def assert_all_equal(*tests):
@@ -19,12 +19,33 @@ def _equal(x, y):
1919
reduce(_equal, tests)
2020

2121

22+
def test_noncons_type():
23+
24+
with pytest.raises(TypeError):
25+
NonCons()
26+
27+
class MyStr(object):
28+
pass
29+
30+
NonCons.register(MyStr)
31+
32+
assert issubclass(MyStr, NonCons)
33+
assert not isinstance(MyStr, NonCons)
34+
assert not issubclass(MyStr, MaybeCons)
35+
assert not issubclass(ConsPair, NonCons)
36+
37+
2238
def test_cons_type():
39+
with pytest.raises(TypeError):
40+
MaybeCons()
41+
2342
assert isinstance(cons(1, "hi"), ConsPair)
2443
assert isinstance((1, 2), ConsPair)
2544
assert isinstance([1, 2], ConsPair)
26-
assert isinstance(OrderedDict({(1): 2}), ConsPair)
45+
assert isinstance(OrderedDict({1: 2}), ConsPair)
2746
assert isinstance(iter([1]), ConsPair)
47+
48+
assert not isinstance({1: 2}, MaybeCons)
2849
assert not isinstance(cons(1, "hi"), MaybeCons)
2950
assert not isinstance({}, ConsPair)
3051
assert not isinstance(set(), ConsPair)
@@ -40,6 +61,10 @@ def test_cons_type():
4061

4162

4263
def test_cons_null():
64+
65+
with pytest.raises(TypeError):
66+
ConsNull()
67+
4368
assert isinstance(None, ConsNull)
4469
assert isinstance([], ConsNull)
4570
assert isinstance(tuple(), ConsNull)
@@ -91,8 +116,8 @@ def test_cons_join():
91116
assert cons("a", cons("b", "c")).cdr == cons("b", "c")
92117

93118

94-
def test_cons_tr():
95-
assert repr(cons(1, 2)) == "ConsPair(1 2)"
119+
def test_cons_str():
120+
assert repr(cons(1, 2)) == "ConsPair(1, 2)"
96121
assert str(cons(1, 2, 3)) == "(1 . (2 . 3))"
97122

98123

@@ -128,7 +153,11 @@ def test_car_cdr():
128153
with pytest.raises(ConsError):
129154
cdr(OrderedDict())
130155

156+
assert car([1, 2]) == 1
157+
assert cdr([1, 2]) == [2]
158+
131159
assert car(cons("a", "b")) == "a"
160+
132161
z = car(cons(iter([]), 1))
133162
expected = iter([])
134163
assert type(z) == type(expected)

0 commit comments

Comments
 (0)