11from functools import reduce
2- from itertools import chain
32from operator import length_hint
43from collections import OrderedDict
5- from collections .abc import Iterator
4+ from itertools import chain , islice
5+ from collections .abc import Iterator , Sequence , ItemsView
66
77from multipledispatch import dispatch
88
99from toolz import last , first
10- from toolz .itertoolz import rest
10+
11+ # We can't use this because `islice` drops __length_hint__ info.
12+ # from toolz.itertoolz import rest
13+
14+
15+ def rest (seq ):
16+ if isinstance (seq , Iterator ) and length_hint (seq , 2 ) <= 1 :
17+ return iter ([])
18+ else :
19+ return islice (seq , 1 , None )
20+
21+
22+ class ConsError (ValueError ):
23+ pass
1124
1225
1326class ConsType (type ):
1427 def __instancecheck__ (self , o ):
15- return is_cons (o )
28+ return (
29+ issubclass (type (o ), (ConsPair , MaybeCons ))
30+ and length_hint (o , 0 ) > 0
31+ )
32+
33+
34+ class ConsNullType (type ):
35+ def __instancecheck__ (self , o ):
36+ if o is None :
37+ return True
38+ elif issubclass (type (o ), MaybeCons ):
39+ lhint = length_hint (o , - 1 )
40+ if lhint == 0 :
41+ return True
42+ elif lhint > 0 :
43+ return False
44+ else :
45+ return None
46+ else :
47+ return False
48+
49+
50+ class ConsNull (metaclass = ConsNullType ):
51+ """A class used to indicate a Lisp/cons-like null.
52+
53+ A "Lisp-like" null object is one that can be used as a `cdr` to produce a
54+ non-`ConsPair` collection (e.g. `None`, `[]`, `()`, `OrderedDict`, etc.)
55+
56+ It's important that this function be used when considering an arbitrary
57+ object as the terminating `cdr` for a given collection (e.g. when unifying
58+ `cons` objects); otherwise, fixed choices for the terminating `cdr`, such
59+ as `None` or `[]`, will severely limit the applicability of the
60+ decomposition.
61+
62+ Also, for relevant collections with no concrete length information, `None`
63+ is returned, and it signifies the uncertainty of the negative assertion.
64+ """
65+
66+ pass
1667
1768
1869class ConsPair (metaclass = ConsType ):
@@ -41,18 +92,40 @@ def __new__(cls, *parts):
4192 elif len (parts ) == 2 :
4293 car_part = first (parts )
4394 cdr_part = last (parts )
44- try :
45- res = cons_merge (car_part , cdr_part )
46- except NotImplementedError :
95+
96+ if cdr_part is None :
97+ cdr_part = []
98+
99+ if isinstance (
100+ cdr_part , (ConsNull , ConsPair , Iterator )
101+ ) and not issubclass (type (cdr_part ), ConsPair ):
102+ res = cls .cons_merge (car_part , cdr_part )
103+ else :
47104 instance = super (ConsPair , cls ).__new__ (cls )
48105 instance .car = car_part
49106 instance .cdr = cdr_part
50107 res = instance
108+
51109 else :
52110 raise ValueError ("Number of arguments must be greater than 2." )
53111
54112 return res
55113
114+ @classmethod
115+ def cons_merge (cls , car_part , cdr_part ):
116+
117+ if isinstance (cdr_part , OrderedDict ):
118+ cdr_part = cdr_part .items ()
119+
120+ res = chain ((car_part ,), cdr_part )
121+
122+ if isinstance (cdr_part , ItemsView ):
123+ res = OrderedDict (res )
124+ elif not isinstance (cdr_part , Iterator ):
125+ res = type (cdr_part )(res )
126+
127+ return res
128+
56129 def __hash__ (self ):
57130 return hash ([self .car , self .cdr ])
58131
@@ -75,74 +148,78 @@ def __str__(self):
75148cons = ConsPair
76149
77150
78- @dispatch (object , type (None ))
79- def cons_merge (car_part , cdr_part ):
80- """Merge a generic car and cdr.
81-
82- This is the base/`nil` case with `cdr` `None`; it produces a standard list.
83- """
84- return [car_part ]
85-
86-
87- @cons_merge .register (object , ConsPair )
88- def cons_merge_ConsPair (car_part , cdr_part ):
89- """Merge a car and a `ConsPair` cdr."""
90- return ConsPair ([car_part , car (cdr_part )], cdr (cdr_part ))
151+ class MaybeConsType (type ):
152+ _ignored_types = (object , type (None ), ConsPair , str )
91153
154+ 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+ )
92160
93- @cons_merge .register (object , Iterator )
94- def cons_merge_Iterator (car_part , cdr_part ):
95- """Merge a car and an `Iterator` cdr."""
96- return chain ([car_part ], cdr_part )
97161
162+ class MaybeCons (metaclass = MaybeConsType ):
163+ """A class used to dynamically determine potential cons types from
164+ non-ConsPairs.
98165
99- @cons_merge .register (object , (list , tuple ))
100- def cons_merge_list_tuple (car_part , cdr_part ):
101- """Merge a car with a list or tuple cdr."""
102- return type (cdr_part )([car_part ]) + cdr_part
166+ For example,
103167
168+ issubclass(tuple, MaybeCons) is True
169+ issubclass(ConsPair, MaybeCons) is False
104170
105- @cons_merge .register ((list , tuple ), OrderedDict )
106- def cons_merge_OrderedDict (car_part , cdr_part ):
107- """Merge a list/tuple car with a dict cdr."""
108- if hasattr (cdr_part , "move_to_end" ):
109- cdr_part .update ([car_part ])
110- cdr_part .move_to_end (first (car_part ), last = False )
111- else :
112- cdr_part = OrderedDict ([car_part ] + list (cdr_part .items ()))
171+ The potential cons types are drawn from the implemented `cdr` dispatch
172+ functions.
173+ """
113174
114- return cdr_part
175+ pass
115176
116177
117- @dispatch (type (None ))
178+ @dispatch (( type (None ), str ))
118179def car (z ):
119- raise TypeError ("Not a cons pair" )
180+ raise ConsError ("Not a cons pair" )
120181
121182
122183@car .register (ConsPair )
123184def car_ConsPair (z ):
124185 return z .car
125186
126187
127- @car .register (( list , tuple , Iterator ) )
188+ @car .register (Iterator )
128189def car_Iterator (z ):
190+ """Return the first element in the given iterator.
191+
192+ Warning: `car` necessarily draws from the iterator, and we can't do
193+ much--within this function--to make copies (e.g. with `tee`) that will
194+ appropriately replace the original iterator.
195+ Callers must handle this themselves.
196+ """
129197 try :
198+ # z, _ = tee(z)
130199 return first (z )
131200 except StopIteration :
132- raise TypeError ("Not a cons pair" )
201+ raise ConsError ("Not a cons pair" )
202+
203+
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" )
133210
134211
135212@car .register (OrderedDict )
136213def car_OrderedDict (z ):
137214 if len (z ) == 0 :
138- raise TypeError ("Not a cons pair" )
215+ raise ConsError ("Not a cons pair" )
139216
140217 return first (z .items ())
141218
142219
143- @dispatch (type (None ))
220+ @dispatch (( type (None ), str ))
144221def cdr (z ):
145- raise TypeError ("Not a cons pair" )
222+ raise ConsError ("Not a cons pair" )
146223
147224
148225@cdr .register (ConsPair )
@@ -153,69 +230,19 @@ def cdr_ConsPair(z):
153230@cdr .register (Iterator )
154231def cdr_Iterator (z ):
155232 if length_hint (z , 1 ) == 0 :
156- raise TypeError ("Not a cons pair" )
233+ raise ConsError ("Not a cons pair" )
157234 return rest (z )
158235
159236
160- @cdr .register (( list , tuple ) )
161- def cdr_list_tuple (z ):
237+ @cdr .register (Sequence )
238+ def cdr_Sequence (z ):
162239 if len (z ) == 0 :
163- raise TypeError ("Not a cons pair" )
164- return type (z )(list ( rest (z ) ))
240+ raise ConsError ("Not a cons pair" )
241+ return type (z )(rest (z ))
165242
166243
167244@cdr .register (OrderedDict )
168245def cdr_OrderedDict (z ):
169246 if len (z ) == 0 :
170- raise TypeError ("Not a cons pair" )
247+ raise ConsError ("Not a cons pair" )
171248 return cdr (list (z .items ()))
172-
173-
174- def is_cons (a ):
175- """Determine if an object is the result of a `cons`.
176-
177- This is automatically determined by the accepted `cdr` types for each
178- `cons_merge` implementation, since any such implementation implies that
179- `cons` can construct that type.
180- """
181- return issubclass (type (a ), ConsPair ) or (
182- any (
183- isinstance (a , d )
184- for _ , d in cons_merge .funcs .keys ()
185- if d not in (object , ConsPair )
186- )
187- and length_hint (a , 0 ) > 0
188- )
189-
190-
191- def is_null (a ):
192- """Check if an object is a "Lisp-like" null.
193-
194- A "Lisp-like" null object is one that can be used as a `cdr` to produce a
195- non-`ConsPair` collection (e.g. `None`, `[]`, `()`, `OrderedDict`, etc.)
196-
197- It's important that this function be used when considering an arbitrary
198- object as the terminating `cdr` for a given collection (e.g. when unifying
199- `cons` objects); otherwise, fixed choices for the terminating `cdr`, such
200- as `None` or `[]`, will severely limit the applicability of the
201- decomposition.
202-
203- Also, for relevant collections with no concrete length information, `None`
204- is returned, and it signifies the uncertainty of the negative assertion.
205- """
206- if a is None :
207- return True
208- elif any (
209- isinstance (a , d )
210- for d , in cdr .funcs .keys ()
211- if not issubclass (d , (ConsPair , type (None )))
212- ):
213- lhint = length_hint (a , - 1 )
214- if lhint == 0 :
215- return True
216- elif lhint > 0 :
217- return False
218- else :
219- return None
220- else :
221- return False
0 commit comments