Skip to content

Commit 8909908

Browse files
committed
Improve coverage; fix some things
- `mogrify` now supports `ll` correctly, `Nil` is no longer processed (technically, this is a breaking change, but `mogrify` wasn't very useful on `cons` structures before this change, so it's unlikely to affect anything) - fix a search-and-misplace (no `wrap` in `_StrReprEqMixin`, it was just `w`) - remove a silly `except` in `frozendict`. As input, we support any mappings that `dict` does; in case it's fed something inappropriate, let the user know. - add missing test cases
1 parent e67b147 commit 8909908

File tree

2 files changed

+135
-25
lines changed

2 files changed

+135
-25
lines changed

unpythonic/collections.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from operator import lt, le, ge, gt
2020
import threading
2121

22-
from .llist import cons
22+
from .llist import cons, Nil
2323
from .misc import getattrrec
2424
from .env import env
2525
from .dynassign import _Dyn
@@ -65,8 +65,7 @@ def mogrify(func, container):
6565
6666
- The ``cons`` container from ``unpythonic.llist`` (including the
6767
``llist`` linked lists). This is treated with the general tree
68-
strategy, so nested linked lists will be flattened, and the
69-
final ``nil`` is also processed.
68+
strategy. Any ``nil`` is passed through as-is.
7069
7170
Any value that does not match any of these is treated as an atom.
7271
@@ -81,7 +80,7 @@ def doit(x):
8180
y = [doit(elt) for elt in x]
8281
if hasattr(x, "clear"):
8382
x.clear() # list has this, but not guaranteed by MutableSequence
84-
else:
83+
else: # pragma: no cover, no realistic `MutableSequence` that is missing `clear`?
8584
while x:
8685
x.pop()
8786
x.extend(y)
@@ -95,7 +94,7 @@ def doit(x):
9594
x.clear()
9695
if hasattr(x, "update"):
9796
x.update(y) # set has this, but not guaranteed by MutableSet
98-
else:
97+
else: # pragma: no cover, no realistic `MutableSet` that is missing `update`?
9998
for elt in y:
10099
x.add(elt)
101100
return x
@@ -110,6 +109,8 @@ def doit(x):
110109
x.set(doit(x.get()))
111110
return x
112111
# immutable containers
112+
elif isinstance(x, Nil): # for unpythonic.llist.ll() support
113+
return x
113114
elif isinstance(x, cons):
114115
return cons(doit(x.car), doit(x.cdr))
115116
elif isinstance(x, Some):
@@ -184,7 +185,7 @@ def f(b):
184185
"""
185186
def __init__(self, x=None):
186187
self.x = x
187-
def __repr__(self):
188+
def __repr__(self): # pragma: no cover
188189
return "box({})".format(repr(self.x))
189190
def __contains__(self, x):
190191
return self.x == x
@@ -234,7 +235,7 @@ class ThreadLocalBox(box):
234235
def __init__(self, x=None):
235236
self.storage = threading.local()
236237
self._default = x
237-
def __repr__(self):
238+
def __repr__(self): # pragma: no cover
238239
"""**WARNING**: the repr shows only the content seen by the current thread."""
239240
return "ThreadLocalBox({})".format(repr(self.get()))
240241
def __contains__(self, x):
@@ -278,7 +279,7 @@ class Some:
278279
"""
279280
def __init__(self, x=None):
280281
self.x = x
281-
def __repr__(self):
282+
def __repr__(self): # pragma: no cover
282283
return "Some({})".format(repr(self.x))
283284
def __contains__(self, x):
284285
return self.x == x
@@ -450,14 +451,11 @@ def __init__(self, *ms, **bindings):
450451
super().__init__()
451452
self._data = {}
452453
for m in ms:
453-
try:
454-
self._data.update(m)
455-
except TypeError:
456-
pass
454+
self._data.update(m)
457455
self._data.update(bindings)
458456

459457
@wraps(dict.__repr__)
460-
def __repr__(self):
458+
def __repr__(self): # pragma: no cover
461459
return "frozendict({})".format(self._data.__repr__())
462460

463461
def __hash__(self):
@@ -534,30 +532,30 @@ class MutableSequenceView(SequenceView):
534532
"""
535533
@abstractmethod
536534
def __setitem__(self, k, v):
537-
pass
535+
pass # pragma: no cover
538536
@abstractmethod
539537
def reverse(self):
540-
pass
538+
pass # pragma: no cover
541539

542540
# -----------------------------------------------------------------------------
543541

544542
class _StrReprEqMixin:
545-
def _lowlevel_repr(self):
543+
def _lowlevel_repr(self): # pragma: no cover
546544
cls = type(getattrrec(self, "seq")) # de-onionize
547545
ctor = tuple if hasattr(cls, "_make") else cls # slice of namedtuple -> tuple
548546
return ctor(x for x in self)
549-
def __str__(self):
547+
def __str__(self): # pragma: no cover
550548
return str(self._lowlevel_repr())
551-
def __repr__(self):
549+
def __repr__(self): # pragma: no cover
552550
return "{:s}({!r})".format(self.__class__.__name__, self._lowlevel_repr())
553551

554552
def __eq__(self, other):
555553
if other is self:
556554
return True
557555
if len(self) != len(other):
558556
return False
559-
for v, wrap in zip(self, other):
560-
if v != wrap:
557+
for v1, v2 in zip(self, other):
558+
if v1 != v2:
561559
return False
562560
return True
563561

@@ -850,18 +848,27 @@ def apply_conversion(k):
850848
def convert(k):
851849
if k is not None:
852850
if not isinstance(k, int):
853-
raise TypeError("k must be int, got {} with value {}".format(type(k), k))
851+
# This is not triggered in the current code because the outer
852+
# layers protect against having to check here, but since the
853+
# `convert` function is returned to the caller, let's be
854+
# careful.
855+
raise TypeError("k must be int, got {} with value {}".format(type(k), k)) # pragma: no cover
854856
# Almost standard semantics for negative indices. Usually -n < k < n,
855857
# but here we must allow for conversion of the end position, for
856858
# which the last valid value is one past the end.
857-
if not -n <= k <= n:
858-
raise IndexError("Should have -n <= k <= n, but n = len(args) = {}, and k = {}".format(n, k))
859+
if n is not None and not -n <= k <= n:
860+
raise IndexError("Should have -n <= k <= n, but n = {}, and k = {}".format(n, k))
859861
return apply_conversion(k) if k < 0 else k
860862
return convert
861863

862864
def _canonize_slice(s, n=None, wrap=None): # convert negatives, inject defaults.
863865
if not isinstance(s, slice):
864-
raise TypeError("s must be slice, got {} with value {}".format(type(s), s))
866+
# Not triggered in the current code, because this is an internal function
867+
# and `in_slice` already checks; but let's be careful in case this is later
868+
# used elsewhere. (And, it's already possible that some internal caller
869+
# incorrectly uses the no-check mode of the internal implementation function
870+
# `_index_in_slice`.)
871+
raise TypeError("s must be slice, got {} with value {}".format(type(s), s)) # pragma: no cover
865872

866873
step = s.step if s.step is not None else +1 # no "s.step or +1"; someone may try step=0
867874
if step == 0:

unpythonic/test/test_collections.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,50 @@
88
import threading
99

1010
from ..collections import (box, ThreadLocalBox, Some, Shim, unbox,
11-
frozendict, view, roview, ShadowedSequence, mogrify)
11+
frozendict, view, roview, ShadowedSequence, mogrify,
12+
in_slice, index_in_slice)
1213
from ..fold import foldr
14+
from ..llist import cons, ll
1315

1416
def runtests():
17+
with testset("internal utilities"):
18+
test[in_slice(5, slice(10))]
19+
test[in_slice(5, 5)] # convenience: int instead of a slice
20+
test[in_slice(5, slice(1, 10, 2))]
21+
test[not in_slice(5, slice(0, 10, 2))]
22+
23+
test[index_in_slice(5, slice(10)) == 5]
24+
test[index_in_slice(5, slice(1, 10, 2)) == 2]
25+
26+
# with sequence length parameter, and with negative indices
27+
test[in_slice(8, slice(0, None, 1), 10)]
28+
test[in_slice(-2, slice(0, None, 1), 10)]
29+
test[in_slice(9, 9, 10)] # convenience
30+
test[in_slice(-1, -1, 10)]
31+
test[in_slice(7, slice(None, None, -1), 10)]
32+
test[in_slice(-3, slice(None, None, -1), 10)]
33+
test[in_slice(7, slice(9, None, -1), 10)]
34+
test[in_slice(-3, slice(9, None, -1), 10)]
35+
36+
test[in_slice(5, slice(0, None, 1), 10)]
37+
test_raises[IndexError, in_slice(5, slice(0, None, 1), 3)] # out of range when length = 3
38+
39+
test[index_in_slice(-1, slice(10), 10) == 9]
40+
test[index_in_slice(-1, slice(None, None, -1), 10) == 0]
41+
test[index_in_slice(7, slice(None, None, -2), 10) == 1]
42+
test[index_in_slice(-3, slice(None, None, -2), 10) == 1]
43+
44+
test_raises[TypeError, in_slice("not an index", slice(10))]
45+
test_raises[TypeError, in_slice(5, "not a slice or int")]
46+
test_raises[TypeError, in_slice(1, slice(10), "not a length")]
47+
test_raises[ValueError, in_slice(1, slice(10), -3)] # negative length
48+
49+
test_raises[ValueError, in_slice(-1, slice(10))] # need length to interpret negative indices
50+
51+
test_raises[ValueError, in_slice(1, slice(0, None, 0))] # zero step
52+
test_raises[ValueError, in_slice(1, slice(0, None, 1))] # missing length for default stop with positive step
53+
test_raises[ValueError, in_slice(1, slice(None, None, -1))] # missing length for default start with negative step
54+
1555
# box: mutable single-item container à la Racket
1656
with testset("box"):
1757
b = box(17)
@@ -76,6 +116,15 @@ def test_threadlocalbox_worker():
76116
t.join()
77117
test[unbox(tlb) == 42] # In the main thread, this box still has the original value.
78118

119+
test[42 in tlb]
120+
test[[x for x in tlb] == [42]]
121+
test[tlb == 42]
122+
test[len(tlb) == 1]
123+
124+
tlb2 = ThreadLocalBox(42)
125+
test[tlb2 is not tlb]
126+
test[tlb2 == tlb]
127+
79128
# The default object can be changed.
80129
tlb = ThreadLocalBox(42)
81130
# We haven't sent any object to the box, so we see the default object.
@@ -116,6 +165,9 @@ def test_threadlocalbox_worker():
116165
test_raises[TypeError, s << 23] # immutable
117166
test_raises[AttributeError, s.set(23)]
118167

168+
test[[x for x in s] == [42]]
169+
test[len(s) == 1]
170+
119171
# Shim (a.k.a. attribute proxy): redirect attribute accesses.
120172
#
121173
# The shim holds a box. Attribute accesses on the shim are redirected
@@ -181,6 +233,8 @@ class Zee:
181233
test[s.z == "hi from Zee"]
182234
test_chaining()
183235

236+
test_raises[TypeError, Shim("la la la")] # not a box, shouldn't be able to Shim it
237+
184238
# frozendict: immutable dictionary (like frozenset, but for dictionaries)
185239
with testset("frozendict"):
186240
d3 = frozendict({'a': 1, 'b': 2})
@@ -320,6 +374,18 @@ class Zee:
320374
test[v == [1, 2, 3, 4, 5]]
321375
test[lst == [42, 0, 1, 2, 3, 4, 5]]
322376

377+
tup = (1, 2, 3, 4, 5)
378+
test_raises[TypeError, view(tup)] # tuple is read-only, view is read/write
379+
380+
lst = list(range(5))
381+
v = view(lst)[2:]
382+
with test_raises(TypeError):
383+
v[2, 3] = 42 # multidimensional indexing not supported
384+
with test_raises(IndexError):
385+
v[9001] = 42
386+
with test_raises(IndexError):
387+
v[-9001] = 42
388+
323389
# read-only live view for sequences
324390
# useful to give read access to a sequence that is an internal detail
325391
with testset("roview"):
@@ -335,6 +401,24 @@ class Zee:
335401
v[2] = 3
336402
test_raises[AttributeError, v.reverse()] # read-only view does not support in-place reverse
337403

404+
tup = tuple(range(5))
405+
v1 = roview(tup)[2:]
406+
test[v1 == v1]
407+
v2 = roview(tup)[2:]
408+
test[v2 is not v1]
409+
test[v2 == v1]
410+
v3 = roview(tup)[3:]
411+
test[v3 != v1]
412+
v4 = roview(tup)[0:2]
413+
v5 = roview(tup)[1:3]
414+
test[v5 != v4]
415+
416+
tup = tuple(range(5))
417+
v1 = roview(tup)[2:]
418+
test_raises[TypeError, v1[2, 3]] # multidimensional indexing not supported
419+
test_raises[IndexError, v1[9001]]
420+
test_raises[IndexError, v1[-9001]]
421+
338422
# sequence shadowing (like ChainMap, but only two levels, and for sequences, not mappings)
339423
with testset("ShadowedSequence"):
340424
tpl = (1, 2, 3, 4, 5)
@@ -343,6 +427,10 @@ class Zee:
343427
test[tpl == (1, 2, 3, 4, 5)]
344428
test[s[2:] == (42, 4, 5)]
345429

430+
test_raises[TypeError, s[2, 3]] # multidimensional indexing not supported
431+
test_raises[IndexError, s[9001]]
432+
test_raises[IndexError, s[-9001]]
433+
346434
s2 = ShadowedSequence(tpl, slice(2, 4), (23, 42))
347435
test[s2 == (1, 2, 23, 42, 5)]
348436
test[tpl == (1, 2, 3, 4, 5)]
@@ -357,6 +445,16 @@ class Zee:
357445
test[s2 == (1, 2, 23, 42, 5)]
358446
test[tpl == (1, 2, 3, 4, 5)]
359447

448+
with test_raises(TypeError):
449+
ShadowedSequence(s4, "la la la", "new value") # not a valid index specification
450+
451+
# no-op ShadowedSequence is allowed
452+
s5 = ShadowedSequence(tpl)
453+
test[s5[3] == 4]
454+
455+
s6 = ShadowedSequence(tpl, slice(2, 4), (23,)) # replacement too short...
456+
test_raises[IndexError, s6[3]] # ...which is detected here
457+
360458
# mogrify: in-place map for various data structures (see docstring for details)
361459
with testset("mogrify"):
362460
double = lambda x: 2 * x
@@ -387,11 +485,16 @@ class Zee:
387485
test[mogrify(double, d.values()) == {4, 8, 12}]
388486
test[d == {1: 2, 3: 4, 5: 6}]
389487

488+
test[mogrify(double, cons(1, 2)) == cons(2, 4)]
489+
test[mogrify(double, ll(1, 2, 3)) == ll(2, 4, 6)]
490+
390491
b = box(17)
391492
b2 = mogrify(double, b)
392493
test[b2 == 34]
393494
test[b2 is b]
394495

496+
test[mogrify(double, Some(21)) == Some(42)]
497+
395498
tup = (1, 2, 3)
396499
tup2 = mogrify(double, tup)
397500
test[tup2 == (2, 4, 6)]

0 commit comments

Comments
 (0)