Skip to content

Commit 5833597

Browse files
authored
Allow builtin Basilisp types to be pickled (#543)
1 parent 29a3f85 commit 5833597

File tree

11 files changed

+130
-11
lines changed

11 files changed

+130
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
* Fixed a bug where aliased Python submodule imports referred to the top-level module rather than the submodule (#533)
1919
* Fixed a bug where static methods and class methods on types created by `deftype` could not be referred to directly (defeating the purpose of the static or class method) (#537)
2020
* Fixed a bug where `defftype` forms could not be declared without at least one field (#540)
21+
* Fixed a bug where not all builtin Basilisp types could be pickled (#518)
2122

2223
## [v0.1.dev13] - 2020-03-16
2324
### Added

src/basilisp/core.lpy

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4467,9 +4467,22 @@
44674467
(defn gen-interface
44684468
"Generate and return a new Python interface (abstract base clase).
44694469

4470+
Options may be specified as key-value pairs. The following options are supported:
4471+
- :name - the name of the interface as a string; required
4472+
- :extends - a vector of interfaces the new interface should extend; optional
4473+
- :methods - an optional vector of method signatures like:
4474+
[ (method-name [args ...] docstring) ... ]
4475+
44704476
Callers should use `definterface` to generate new interfaces."
4471-
[interface-name methods]
4472-
(let [methods (reduce (fn [m [method-name args docstring]]
4477+
[& opts]
4478+
(let [opt-map (apply hash-map opts)
4479+
interface-name (:name opt-map)
4480+
extends (as-> (:extends opt-map []) $
4481+
(remove #(identical? abc/ABC %) $)
4482+
(concat $ [abc/ABC])
4483+
(python/tuple $))
4484+
4485+
methods (reduce (fn [m [method-name args docstring]]
44734486
(let [method-args (->> (concat ['^:no-warn-when-unused self] args)
44744487
(apply vector))
44754488
method (->> (list 'fn* method-name method-args)
@@ -4479,10 +4492,10 @@
44794492
(set! (.- method __doc__) docstring))
44804493
(assoc m (munge method-name) method)))
44814494
{}
4482-
methods)]
4495+
(:methods opt-map))]
44834496
(python/type interface-name
4484-
#py (abc/ABC)
4485-
(lisp->py methods))))
4497+
extends
4498+
(lisp->py methods))))
44864499

44874500
;; The behavior described below where `definterface` forms are not permitted
44884501
;; to be used in `deftype` and `defrecord` forms when they are not top-level
@@ -4513,7 +4526,8 @@
45134526
(let [name-str (name interface-name)
45144527
method-sigs (map #(list 'quote %) methods)]
45154528
`(def ~interface-name
4516-
(gen-interface ~name-str [~@method-sigs]))))
4529+
(gen-interface :name ~name-str
4530+
:methods [~@method-sigs]))))
45174531

45184532
;;;;;;;;;;;;;;;;
45194533
;; Data Types ;;

src/basilisp/lang/keyword.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ def _lrepr(self, **kwargs) -> str:
2929
return ":{name}".format(name=self._name)
3030

3131
def __eq__(self, other):
32-
return self is other
32+
return self is other or (
33+
isinstance(other, Keyword)
34+
and (self._name, self._ns) == (other._name, other._ns)
35+
)
3336

3437
def __hash__(self):
3538
return hash((self._name, self._ns))
@@ -40,6 +43,9 @@ def __call__(self, m: IAssociative, default=None):
4043
except AttributeError:
4144
return None
4245

46+
def __reduce__(self):
47+
return keyword, (self._name, self._ns)
48+
4349

4450
def complete(
4551
text: str, kw_cache: atom.Atom["PMap[int, Keyword]"] = __INTERN

src/basilisp/lang/map.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ def __contains__(self, item):
4646
def __eq__(self, other):
4747
return self._inner == other
4848

49-
def __getattr__(self, item):
50-
return getattr(self._inner, item)
51-
5249
def __getitem__(self, item):
5350
return self._inner[item]
5451

tests/basilisp/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import sys
2+
3+
import pytest
4+
5+
6+
@pytest.fixture(params=[3, 4] if sys.version_info < (3, 8) else [3, 4, 5])
7+
def pickle_protocol(request) -> int:
8+
return request.param

tests/basilisp/keyword_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pickle
2+
13
import pytest
24
from pyrsistent import PMap, pmap
35

@@ -47,6 +49,19 @@ def test_keyword_as_function():
4749
assert None is kw(lmap.map({"hi": kw}))
4850

4951

52+
@pytest.mark.parametrize(
53+
"o",
54+
[
55+
keyword("kw1"),
56+
keyword("very-long-name"),
57+
keyword("kw1", ns="namespaced.keyword"),
58+
keyword("long-named-kw", ns="also.namespaced.keyword"),
59+
],
60+
)
61+
def test_keyword_pickleability(pickle_protocol: int, o: Keyword):
62+
assert o == pickle.loads(pickle.dumps(o, protocol=pickle_protocol))
63+
64+
5065
class TestKeywordCompletion:
5166
@pytest.fixture
5267
def empty_cache(self) -> Atom["PMap[int, Keyword]"]:

tests/basilisp/list_test.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import pickle
2+
13
import pytest
24

35
import basilisp.lang.list as llist
@@ -95,6 +97,20 @@ def test_list_rest():
9597
assert llist.l(keyword("kw1"), keyword("kw2")).rest == llist.l(keyword("kw2"))
9698

9799

100+
@pytest.mark.parametrize(
101+
"o",
102+
[
103+
llist.l(),
104+
llist.l(keyword("kw1")),
105+
llist.l(keyword("kw1"), 2),
106+
llist.l(keyword("kw1"), 2, None, "nothingness"),
107+
llist.l(keyword("kw1"), llist.l("string", 4)),
108+
],
109+
)
110+
def test_list_pickleability(pickle_protocol: int, o: llist.List):
111+
assert o == pickle.loads(pickle.dumps(o, protocol=pickle_protocol))
112+
113+
98114
@pytest.mark.parametrize(
99115
"l,str_repr",
100116
[

tests/basilisp/map_test.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pickle
12
from typing import Mapping
23

34
import pytest
@@ -181,3 +182,16 @@ def test_hash_map_creator():
181182

182183
with pytest.raises(IndexError):
183184
lmap.hash_map(1, 2, 3)
185+
186+
187+
@pytest.mark.parametrize(
188+
"o",
189+
[
190+
lmap.m(),
191+
lmap.map({"a": 2}),
192+
lmap.map({"a": 2, None: "NOTHINGNESS"}),
193+
lmap.map({"a": 2, keyword("b"): lmap.map({keyword("c"): "string"})}),
194+
],
195+
)
196+
def test_map_pickleability(pickle_protocol: int, o: lmap.Map):
197+
assert o == pickle.loads(pickle.dumps(o, protocol=pickle_protocol))

tests/basilisp/set_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pickle
12
import typing
23

34
import pytest
@@ -62,6 +63,20 @@ def test_set_with_meta():
6263
assert s4.meta == lmap.m(tag=keyword("macro"))
6364

6465

66+
@pytest.mark.parametrize(
67+
"o",
68+
[
69+
lset.s(),
70+
lset.s(keyword("kw1")),
71+
lset.s(keyword("kw1"), 2),
72+
lset.s(keyword("kw1"), 2, None, "nothingness"),
73+
lset.s(keyword("kw1"), lset.s("string", 4)),
74+
],
75+
)
76+
def test_set_pickleability(pickle_protocol: int, o: lset.Set):
77+
assert o == pickle.loads(pickle.dumps(o, protocol=pickle_protocol))
78+
79+
6580
@pytest.mark.parametrize(
6681
"l,str_repr",
6782
[

tests/basilisp/symbol_test.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import pickle
2+
3+
import pytest
4+
15
import basilisp.lang.map as lmap
26
from basilisp.lang.keyword import keyword
3-
from basilisp.lang.symbol import symbol
7+
from basilisp.lang.symbol import Symbol, symbol
48

59

610
def test_symbol_name_and_ns():
@@ -40,6 +44,19 @@ def test_symbol_with_meta():
4044
assert sym3.meta == lmap.m(tag=keyword("macro"))
4145

4246

47+
@pytest.mark.parametrize(
48+
"o",
49+
[
50+
symbol("kw1"),
51+
symbol("very-long-name"),
52+
symbol("kw1", ns="namespaced.keyword"),
53+
symbol("long-named-kw", ns="also.namespaced.keyword"),
54+
],
55+
)
56+
def test_symbol_pickleability(pickle_protocol: int, o: Symbol):
57+
assert o == pickle.loads(pickle.dumps(o, protocol=pickle_protocol))
58+
59+
4360
def test_symbol_str_and_repr():
4461
sym = symbol("sym", ns="ns")
4562
assert str(sym) == "ns/sym"

0 commit comments

Comments
 (0)