Skip to content

Commit bacd516

Browse files
authored
Remove Atomos as a dependency in favor of readerwriterlock (#624)
* Remove Atomos as a dependency in favor of readerwriterlock * update PR link * Oops * Install toml extra for coverage * Slight changes * Fool of a took
1 parent 939549f commit bacd516

File tree

7 files changed

+98
-66
lines changed

7 files changed

+98
-66
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
* PyTest is now an optional extra dependency, rather than a required dependency (#622)
1313

1414
### Removed
15-
* Removed Click as a dependency in favor of builtin `argparse` (#622)
15+
* Removed Click as a dependency in favor of builtin `argparse` (#622, #624)
16+
* Removed Atomos as a dependency in favor of `readerwriterlock` (#624)
1617

1718
## [v0.1.dev15]
1819
### Added

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ include = ["README.md", "LICENSE"]
3131
[tool.poetry.dependencies]
3232
python = "^3.6"
3333
astor = ">=0.8"
34-
atomos = "*"
3534
attrs = "*"
3635
immutables = "~0.15"
37-
prompt-toolkit = "~=3.0.0"
36+
prompt-toolkit = "~3.0.0"
3837
pyrsistent = "*"
3938
python-dateutil = "*"
39+
readerwriterlock = "~1.0.8"
4040

4141
[tool.poetry.dev-dependencies]
4242
black = "*"
@@ -55,7 +55,7 @@ pygments = ["pygments"]
5555
pytest = ["pytest"]
5656

5757
[tool.poetry.scripts]
58-
basilisp = "basilisp.cli:cli"
58+
basilisp = "basilisp.cli:invoke_cli"
5959

6060
[tool.poetry.plugins.pytest11]
6161
basilisp_test_runner = "basilisp.testrunner"

src/basilisp/lang/atom.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import threading
21
from typing import Callable, Generic, Optional, TypeVar
32

4-
import atomos.atom
3+
from readerwriterlock.rwlock import RWLockFair
54

65
from basilisp.lang.interfaces import IDeref, IPersistentMap
76
from basilisp.lang.reference import ReferenceBase
@@ -10,22 +9,41 @@
109

1110

1211
class Atom(IDeref[T], ReferenceBase, Generic[T]):
13-
__slots__ = ("_atom", "_meta", "_lock")
12+
__slots__ = ("_meta", "_state", "_rlock", "_wlock")
1413

1514
# pylint: disable=assigning-non-slot
1615
def __init__(self, state: T) -> None:
17-
self._atom = atomos.atom.Atom(state)
1816
self._meta: Optional[IPersistentMap] = None
19-
self._lock = threading.Lock()
20-
21-
def compare_and_set(self, old, new):
22-
return self._atom.compare_and_set(old, new)
17+
self._state = state
18+
lock = RWLockFair()
19+
self._rlock = lock.gen_rlock()
20+
self._wlock = lock.gen_wlock()
21+
22+
def compare_and_set(self, old: T, new: T) -> bool:
23+
"""Compare the current state of the Atom to `old`. If the value is the same,
24+
atomically set the value of the state of Atom to `new`. Return True if the
25+
value was swapped. Return False otherwise."""
26+
with self._wlock:
27+
if self._state != old:
28+
return False
29+
self._state = new
30+
return True
2331

2432
def deref(self) -> T:
25-
return self._atom.deref()
33+
"""Return the state stored within the Atom."""
34+
with self._rlock:
35+
return self._state
2636

2737
def reset(self, v: T) -> T:
28-
return self._atom.reset(v)
38+
"""Reset the state of the Atom to `v` without regard to the current value."""
39+
with self._wlock:
40+
self._state = v
41+
return v
2942

3043
def swap(self, f: Callable[..., T], *args, **kwargs) -> T:
31-
return self._atom.swap(f, *args, **kwargs)
44+
"""Atomically swap the state of the Atom to the return value of
45+
`f(old, *args, **kwargs)`, returning the new value."""
46+
with self._wlock:
47+
newval = f(self._state, *args, **kwargs)
48+
self._state = newval
49+
return newval

src/basilisp/lang/reference.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import threading
21
from typing import Callable, Optional
32

3+
from readerwriterlock.rwlock import Lockable
4+
45
from basilisp.lang.interfaces import IPersistentMap, IReference
56

67
try:
@@ -21,20 +22,21 @@ class ReferenceBase(IReference):
2122
2223
Consumers must have a `_lock` and `_meta` property defined."""
2324

24-
_lock: threading.Lock
25+
_rlock: Lockable
26+
_wlock: Lockable
2527
_meta: Optional[IPersistentMap]
2628

2729
@property
2830
def meta(self) -> Optional[IPersistentMap]:
29-
with self._lock:
31+
with self._rlock:
3032
return self._meta
3133

3234
def alter_meta(self, f: AlterMeta, *args) -> IPersistentMap:
33-
with self._lock:
35+
with self._wlock:
3436
self._meta = f(self._meta, *args)
3537
return self._meta
3638

3739
def reset_meta(self, meta: IPersistentMap) -> IPersistentMap:
38-
with self._lock:
40+
with self._wlock:
3941
self._meta = meta
4042
return meta

src/basilisp/lang/runtime.py

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
cast,
3232
)
3333

34+
from readerwriterlock.rwlock import RWLockFair
35+
3436
from basilisp.lang import keyword as kw
3537
from basilisp.lang import list as llist
3638
from basilisp.lang import map as lmap
@@ -223,7 +225,8 @@ class Var(IDeref, ReferenceBase):
223225
"_is_bound",
224226
"_tl",
225227
"_meta",
226-
"_lock",
228+
"_rlock",
229+
"_wlock",
227230
)
228231

229232
def __init__(
@@ -240,7 +243,9 @@ def __init__(
240243
self._is_bound = False
241244
self._tl = None
242245
self._meta = meta
243-
self._lock = threading.Lock()
246+
lock = RWLockFair()
247+
self._rlock = lock.gen_rlock()
248+
self._wlock = lock.gen_wlock()
244249

245250
if dynamic:
246251
self._tl = _VarBindings()
@@ -268,10 +273,6 @@ def name(self) -> sym.Symbol:
268273
def dynamic(self) -> bool:
269274
return self._dynamic
270275

271-
@dynamic.setter
272-
def dynamic(self, is_dynamic: bool):
273-
self._dynamic = is_dynamic
274-
275276
@property
276277
def is_private(self) -> Optional[bool]:
277278
if self._meta is not None:
@@ -282,20 +283,25 @@ def is_private(self) -> Optional[bool]:
282283
def is_bound(self) -> bool:
283284
return self._is_bound or self.is_thread_bound
284285

286+
def _set_root(self, val) -> None:
287+
# Private setter which does not include lock so it can be used
288+
# by other properties and methods which already manage the lock.
289+
self._is_bound = True
290+
self._root = val
291+
285292
@property
286293
def root(self):
287-
with self._lock:
294+
with self._rlock:
288295
return self._root
289296

290297
@root.setter
291298
def root(self, val):
292-
with self._lock:
293-
self._is_bound = True
294-
self._root = val
299+
with self._wlock:
300+
self._set_root(val)
295301

296302
def alter_root(self, f, *args):
297-
with self._lock:
298-
self._root = f(self._root, *args)
303+
with self._wlock:
304+
self._set_root(f(self._root, *args))
299305

300306
def push_bindings(self, val):
301307
if self._tl is None:
@@ -316,22 +322,24 @@ def is_thread_bound(self):
316322

317323
@property
318324
def value(self):
319-
if self._dynamic:
320-
assert self._tl is not None
321-
if len(self._tl.bindings) > 0:
322-
return self._tl.bindings[-1]
323-
return self.root
325+
with self._rlock:
326+
if self._dynamic:
327+
assert self._tl is not None
328+
if len(self._tl.bindings) > 0:
329+
return self._tl.bindings[-1]
330+
return self._root
324331

325332
@value.setter
326333
def value(self, v):
327-
if self._dynamic:
328-
assert self._tl is not None
329-
if len(self._tl.bindings) > 0:
330-
self._tl.bindings[-1] = v
331-
else:
332-
self.push_bindings(v)
333-
return
334-
self.root = v
334+
with self._wlock:
335+
if self._dynamic:
336+
assert self._tl is not None
337+
if len(self._tl.bindings) > 0:
338+
self._tl.bindings[-1] = v
339+
else:
340+
self.push_bindings(v)
341+
return
342+
self._set_root(v)
335343

336344
@staticmethod
337345
def intern(
@@ -496,7 +504,8 @@ class Namespace(ReferenceBase):
496504
"_name",
497505
"_module",
498506
"_meta",
499-
"_lock",
507+
"_rlock",
508+
"_wlock",
500509
"_interns",
501510
"_refers",
502511
"_aliases",
@@ -509,7 +518,9 @@ def __init__(self, name: sym.Symbol, module: BasilispModule = None) -> None:
509518
self._module = Maybe(module).or_else(lambda: _new_module(name.as_python_sym()))
510519

511520
self._meta: Optional[IPersistentMap] = None
512-
self._lock = threading.Lock()
521+
lock = RWLockFair()
522+
self._rlock = lock.gen_rlock()
523+
self._wlock = lock.gen_wlock()
513524

514525
self._aliases: NamespaceMap = lmap.PersistentMap.empty()
515526
self._imports: ModuleMap = lmap.map(
@@ -545,36 +556,36 @@ def module(self, m: BasilispModule):
545556
def aliases(self) -> NamespaceMap:
546557
"""A mapping between a symbolic alias and another Namespace. The
547558
fully qualified name of a namespace is also an alias for itself."""
548-
with self._lock:
559+
with self._rlock:
549560
return self._aliases
550561

551562
@property
552563
def imports(self) -> ModuleMap:
553564
"""A mapping of names to Python modules imported into the current
554565
namespace."""
555-
with self._lock:
566+
with self._rlock:
556567
return self._imports
557568

558569
@property
559570
def import_aliases(self) -> AliasMap:
560571
"""A mapping of a symbolic alias and a Python module name."""
561-
with self._lock:
572+
with self._rlock:
562573
return self._import_aliases
563574

564575
@property
565576
def interns(self) -> VarMap:
566577
"""A mapping between a symbolic name and a Var. The Var may point to
567578
code, data, or nothing, if it is unbound. Vars in `interns` are
568579
interned in _this_ namespace."""
569-
with self._lock:
580+
with self._rlock:
570581
return self._interns
571582

572583
@property
573584
def refers(self) -> VarMap:
574585
"""A mapping between a symbolic name and a Var. Vars in refers are
575586
interned in another namespace and are only referred to without an
576587
alias in this namespace."""
577-
with self._lock:
588+
with self._rlock:
578589
return self._refers
579590

580591
def __repr__(self):
@@ -605,41 +616,41 @@ def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:
605616

606617
def add_alias(self, namespace: "Namespace", *aliases: sym.Symbol) -> None:
607618
"""Add Symbol aliases for the given Namespace."""
608-
with self._lock:
619+
with self._wlock:
609620
new_m = self._aliases
610621
for alias in aliases:
611622
new_m = new_m.assoc(alias, namespace)
612623
self._aliases = new_m
613624

614625
def get_alias(self, alias: sym.Symbol) -> "Optional[Namespace]":
615626
"""Get the Namespace aliased by Symbol or None if it does not exist."""
616-
with self._lock:
627+
with self._rlock:
617628
return self._aliases.val_at(alias, None)
618629

619630
def remove_alias(self, alias: sym.Symbol) -> None:
620631
"""Remove the Namespace aliased by Symbol. Return None."""
621-
with self._lock:
632+
with self._wlock:
622633
self._aliases = self._aliases.dissoc(alias)
623634

624635
def intern(self, sym: sym.Symbol, var: Var, force: bool = False) -> Var:
625636
"""Intern the Var given in this namespace mapped by the given Symbol.
626637
If the Symbol already maps to a Var, this method _will not overwrite_
627638
the existing Var mapping unless the force keyword argument is given
628639
and is True."""
629-
with self._lock:
640+
with self._wlock:
630641
old_var = self._interns.val_at(sym, None)
631642
if old_var is None or force:
632643
self._interns = self._interns.assoc(sym, var)
633644
return self._interns.val_at(sym)
634645

635646
def unmap(self, sym: sym.Symbol) -> None:
636-
with self._lock:
647+
with self._wlock:
637648
self._interns = self._interns.dissoc(sym)
638649

639650
def find(self, sym: sym.Symbol) -> Optional[Var]:
640651
"""Find Vars mapped by the given Symbol input or None if no Vars are
641652
mapped by that Symbol."""
642-
with self._lock:
653+
with self._rlock:
643654
v = self._interns.val_at(sym, None)
644655
if v is None:
645656
return self._refers.val_at(sym, None)
@@ -648,7 +659,7 @@ def find(self, sym: sym.Symbol) -> Optional[Var]:
648659
def add_import(self, sym: sym.Symbol, module: Module, *aliases: sym.Symbol) -> None:
649660
"""Add the Symbol as an imported Symbol in this Namespace. If aliases are given,
650661
the aliases will be applied to the"""
651-
with self._lock:
662+
with self._wlock:
652663
self._imports = self._imports.assoc(sym, module)
653664
if aliases:
654665
m = self._import_aliases
@@ -662,7 +673,7 @@ def get_import(self, sym: sym.Symbol) -> Optional[BasilispModule]:
662673
663674
First try to resolve a module directly with the given name. If no module
664675
can be resolved, attempt to resolve the module using import aliases."""
665-
with self._lock:
676+
with self._rlock:
666677
mod = self._imports.val_at(sym, None)
667678
if mod is None:
668679
alias = self._import_aliases.get(sym, None)
@@ -674,17 +685,17 @@ def get_import(self, sym: sym.Symbol) -> Optional[BasilispModule]:
674685
def add_refer(self, sym: sym.Symbol, var: Var) -> None:
675686
"""Refer var in this namespace under the name sym."""
676687
if not var.is_private:
677-
with self._lock:
688+
with self._wlock:
678689
self._refers = self._refers.assoc(sym, var)
679690

680691
def get_refer(self, sym: sym.Symbol) -> Optional[Var]:
681692
"""Get the Var referred by Symbol or None if it does not exist."""
682-
with self._lock:
693+
with self._rlock:
683694
return self._refers.val_at(sym, None)
684695

685696
def refer_all(self, other_ns: "Namespace") -> None:
686697
"""Refer all the Vars in the other namespace."""
687-
with self._lock:
698+
with self._wlock:
688699
final_refers = self._refers
689700
for s, var in other_ns.interns.items():
690701
if not var.is_private:

tests/basilisp/atom_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_atom():
2222
assert vec.v(1, 2) == a.swap(lambda v, e: v.cons(e), 2)
2323
assert vec.v(1, 2) == a.deref()
2424

25-
assert vec.v(1, 2) == a.reset(lmap.PersistentMap.empty())
25+
assert lmap.PersistentMap.empty() == a.reset(lmap.PersistentMap.empty())
2626
assert lmap.PersistentMap.empty() == a.deref()
2727

2828

0 commit comments

Comments
 (0)