Skip to content

Remove python-dateutil and readerwriterlock dependencies #976

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
* Improved on the nREPL server exception messages by matching that of the REPL user friendly format (#968)

### Removed
* Removed `python-dateutil` and `readerwriterlock` as dependencies, switching to standard library components instead (#976)

### Other
* Run PyPy CI checks on Github Actions rather than CircleCI (#971)

Expand Down
12 changes: 5 additions & 7 deletions pyproject.toml
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loosen version constraints on some of these dependencies.

Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,13 @@ include = ["README.md", "LICENSE"]
python = "^3.8"
attrs = ">=20.2.0"
immutables = ">=0.20,<1.0.0"
prompt-toolkit = "^3.0.0"
prompt-toolkit = ">=3.0.0,<4.0.0"
pyrsistent = ">=0.18.0,<1.0.0"
python-dateutil = "^2.8.1"
readerwriterlock = "^1.0.8"
typing-extensions = "^4.7.0"
typing-extensions = ">=4.7.0,<5.0.0"

astor = { version = "^0.8.1", python = "<3.9", optional = true }
pytest = { version = "^7.0.0", optional = true }
pygments = { version = "^2.9.0", optional = true }
pytest = { version = ">=7.0.0,<9.0.0", optional = true }
pygments = { version = ">=2.9.0,<3.0.0", optional = true }

[tool.poetry.group.dev.dependencies]
black = ">=24.0.0"
Expand All @@ -51,7 +49,7 @@ docutils = [
]
isort = "*"
pygments = "*"
pytest = "^7.0.0"
pytest = ">=7.0.0,<9.0.0"
pytest-pycharm = "*"
# Ensure the Sphinx version remains synchronized with docs/requirements.txt
# to maintain consistent output during both development and publishing on
Expand Down
8 changes: 4 additions & 4 deletions src/basilisp/lang/atom.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import threading
from typing import Callable, Generic, Optional, TypeVar

from readerwriterlock.rwlock import RWLockFair
from typing_extensions import Concatenate, ParamSpec

from basilisp.lang.interfaces import IPersistentMap, RefValidator
Expand All @@ -22,15 +22,15 @@ def __init__(
) -> None:
self._meta: Optional[IPersistentMap] = meta
self._state = state
self._lock = RWLockFair()
self._lock = threading.RLock()
self._watches = PersistentMap.empty()
self._validator = validator

if validator is not None:
self._validate(state)

def _compare_and_set(self, old: T, new: T) -> bool:
with self._lock.gen_wlock():
with self._lock:
if self._state != old:
return False
self._state = new
Expand All @@ -48,7 +48,7 @@ def compare_and_set(self, old: T, new: T) -> bool:

def deref(self) -> T:
"""Return the state stored within the Atom."""
with self._lock.gen_rlock():
with self._lock:
return self._state

def reset(self, v: T) -> T:
Expand Down
30 changes: 11 additions & 19 deletions src/basilisp/lang/reference.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import threading
from typing import Any, Callable, Optional, TypeVar

from readerwriterlock.rwlock import RWLockable
from typing_extensions import Concatenate, ParamSpec

from basilisp.lang import keyword as kw
Expand All @@ -27,21 +27,21 @@ class ReferenceBase(IReference):

Implementers must have the `_lock` and `_meta` properties defined."""

_lock: RWLockable
_lock: threading.RLock
_meta: Optional[IPersistentMap]

@property
def meta(self) -> Optional[IPersistentMap]:
with self._lock.gen_rlock():
with self._lock:
return self._meta

def alter_meta(self, f: AlterMeta, *args) -> Optional[IPersistentMap]:
with self._lock.gen_wlock():
with self._lock:
self._meta = f(self._meta, *args)
return self._meta

def reset_meta(self, meta: Optional[IPersistentMap]) -> Optional[IPersistentMap]:
with self._lock.gen_wlock():
with self._lock:
self._meta = meta
return meta

Expand All @@ -63,7 +63,7 @@ class RefBase(IRef[T], ReferenceBase):
_watches: IPersistentMap[RefWatchKey, RefWatcher[T]]

def add_watch(self, k: RefWatchKey, wf: RefWatcher[T]) -> "RefBase[T]":
with self._lock.gen_wlock():
with self._lock:
self._watches = self._watches.assoc(k, wf)
return self

Expand All @@ -72,26 +72,18 @@ def _notify_watches(self, old: T, new: T) -> None:
wf(k, self, old, new)

def remove_watch(self, k: RefWatchKey) -> "RefBase[T]":
with self._lock.gen_wlock():
with self._lock:
self._watches = self._watches.dissoc(k)
return self

def get_validator(self) -> Optional[RefValidator[T]]:
return self._validator

def set_validator(self, vf: Optional[RefValidator[T]] = None) -> None:
# We cannot use a write lock here since we're calling `self.deref()` which
# attempts to acquire the read lock for the Ref and will deadlock if the
# lock is not reentrant.
#
# There are no guarantees that the Ref lock is reentrant and the default
# locks for Atoms and Vars are not).
#
# This is probably ok for most cases since we expect contention is low or
# non-existent while setting a validator function.
if vf is not None:
self._validate(self.deref(), vf=vf)
self._validator = vf
with self._lock:
if vf is not None:
self._validate(self.deref(), vf=vf)
self._validator = vf

def _validate(self, val: Any, vf: Optional[RefValidator[T]] = None) -> None:
vf = vf or self._validator
Expand Down
50 changes: 24 additions & 26 deletions src/basilisp/lang/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
cast,
)

from readerwriterlock.rwlock import RWLockFair

from basilisp.lang import keyword as kw
from basilisp.lang import list as llist
from basilisp.lang import map as lmap
Expand Down Expand Up @@ -266,7 +264,7 @@ def __init__(
self._is_bound = False
self._tl = None
self._meta = meta
self._lock = RWLockFair()
self._lock = threading.RLock()
self._watches = lmap.PersistentMap.empty()
self._validator = None

Expand Down Expand Up @@ -300,7 +298,7 @@ def dynamic(self) -> bool:
return self._dynamic

def set_dynamic(self, dynamic: bool) -> None:
with self._lock.gen_wlock():
with self._lock:
if dynamic == self._dynamic:
return

Expand Down Expand Up @@ -328,18 +326,18 @@ def _set_root(self, newval) -> None:

@property
def root(self):
with self._lock.gen_rlock():
with self._lock:
return self._root

def bind_root(self, val) -> None:
"""Atomically update the root binding of this Var to val."""
with self._lock.gen_wlock():
with self._lock:
self._set_root(val)

def alter_root(self, f, *args) -> None:
"""Atomically alter the root binding of this Var to the result of calling
f with the existing root value and any additional arguments."""
with self._lock.gen_wlock():
with self._lock:
self._set_root(f(self._root, *args))

def push_bindings(self, val):
Expand All @@ -366,7 +364,7 @@ def value(self):

For non-dynamic Vars, this will just be the root. For dynamic Vars, this will
be any thread-local binding if one is defined. Otherwise, the root value."""
with self._lock.gen_rlock():
with self._lock:
if self._dynamic:
assert self._tl is not None
if len(self._tl.bindings) > 0:
Expand All @@ -378,7 +376,7 @@ def set_value(self, v) -> None:

If the Var is not dynamic, this is equivalent to binding the root value. If the
Var is dynamic, this will set the thread-local bindings for the Var."""
with self._lock.gen_wlock():
with self._lock:
if self._dynamic:
assert self._tl is not None
self._validate(v)
Expand Down Expand Up @@ -585,7 +583,7 @@ def __init__(
self._module = Maybe(module).or_else(lambda: _new_module(name.as_python_sym()))

self._meta: Optional[IPersistentMap] = None
self._lock = RWLockFair()
self._lock = threading.RLock()

self._aliases: NamespaceMap = lmap.PersistentMap.empty()
self._imports: ModuleMap = lmap.map(
Expand Down Expand Up @@ -621,36 +619,36 @@ def module(self, m: BasilispModule):
def aliases(self) -> NamespaceMap:
"""A mapping between a symbolic alias and another Namespace. The
fully qualified name of a namespace is also an alias for itself."""
with self._lock.gen_rlock():
with self._lock:
return self._aliases

@property
def imports(self) -> ModuleMap:
"""A mapping of names to Python modules imported into the current
namespace."""
with self._lock.gen_rlock():
with self._lock:
return self._imports

@property
def import_aliases(self) -> AliasMap:
"""A mapping of a symbolic alias and a Python module name."""
with self._lock.gen_rlock():
with self._lock:
return self._import_aliases

@property
def interns(self) -> VarMap:
"""A mapping between a symbolic name and a Var. The Var may point to
code, data, or nothing, if it is unbound. Vars in `interns` are
interned in _this_ namespace."""
with self._lock.gen_rlock():
with self._lock:
return self._interns

@property
def refers(self) -> VarMap:
"""A mapping between a symbolic name and a Var. Vars in refers are
interned in another namespace and are only referred to without an
alias in this namespace."""
with self._lock.gen_rlock():
with self._lock:
return self._refers

def __repr__(self):
Expand Down Expand Up @@ -681,41 +679,41 @@ def require(self, ns_name: str, *aliases: sym.Symbol) -> BasilispModule:

def add_alias(self, namespace: "Namespace", *aliases: sym.Symbol) -> None:
"""Add Symbol aliases for the given Namespace."""
with self._lock.gen_wlock():
with self._lock:
new_m = self._aliases
for alias in aliases:
new_m = new_m.assoc(alias, namespace)
self._aliases = new_m

def get_alias(self, alias: sym.Symbol) -> "Optional[Namespace]":
"""Get the Namespace aliased by Symbol or None if it does not exist."""
with self._lock.gen_rlock():
with self._lock:
return self._aliases.val_at(alias, None)

def remove_alias(self, alias: sym.Symbol) -> None:
"""Remove the Namespace aliased by Symbol. Return None."""
with self._lock.gen_wlock():
with self._lock:
self._aliases = self._aliases.dissoc(alias)

def intern(self, sym: sym.Symbol, var: Var, force: bool = False) -> Var:
"""Intern the Var given in this namespace mapped by the given Symbol.
If the Symbol already maps to a Var, this method _will not overwrite_
the existing Var mapping unless the force keyword argument is given
and is True."""
with self._lock.gen_wlock():
with self._lock:
old_var = self._interns.val_at(sym, None)
if old_var is None or force:
self._interns = self._interns.assoc(sym, var)
return self._interns.val_at(sym)

def unmap(self, sym: sym.Symbol) -> None:
with self._lock.gen_wlock():
with self._lock:
self._interns = self._interns.dissoc(sym)

def find(self, sym: sym.Symbol) -> Optional[Var]:
"""Find Vars mapped by the given Symbol input or None if no Vars are
mapped by that Symbol."""
with self._lock.gen_rlock():
with self._lock:
v = self._interns.val_at(sym, None)
if v is None:
return self._refers.val_at(sym, None)
Expand All @@ -724,7 +722,7 @@ def find(self, sym: sym.Symbol) -> Optional[Var]:
def add_import(self, sym: sym.Symbol, module: Module, *aliases: sym.Symbol) -> None:
"""Add the Symbol as an imported Symbol in this Namespace. If aliases are given,
the aliases will be applied to the"""
with self._lock.gen_wlock():
with self._lock:
self._imports = self._imports.assoc(sym, module)
if aliases:
m = self._import_aliases
Expand All @@ -738,7 +736,7 @@ def get_import(self, sym: sym.Symbol) -> Optional[BasilispModule]:

First try to resolve a module directly with the given name. If no module
can be resolved, attempt to resolve the module using import aliases."""
with self._lock.gen_rlock():
with self._lock:
mod = self._imports.val_at(sym, None)
if mod is None:
alias = self._import_aliases.get(sym, None)
Expand All @@ -750,17 +748,17 @@ def get_import(self, sym: sym.Symbol) -> Optional[BasilispModule]:
def add_refer(self, sym: sym.Symbol, var: Var) -> None:
"""Refer var in this namespace under the name sym."""
if not var.is_private:
with self._lock.gen_wlock():
with self._lock:
self._refers = self._refers.assoc(sym, var)

def get_refer(self, sym: sym.Symbol) -> Optional[Var]:
"""Get the Var referred by Symbol or None if it does not exist."""
with self._lock.gen_rlock():
with self._lock:
return self._refers.val_at(sym, None)

def refer_all(self, other_ns: "Namespace") -> None:
"""Refer all the Vars in the other namespace."""
with self._lock.gen_wlock():
with self._lock:
final_refers = self._refers
for s, var in other_ns.interns.items():
if not var.is_private:
Expand Down
4 changes: 1 addition & 3 deletions src/basilisp/lang/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
from fractions import Fraction
from typing import Iterable, Match, Pattern, Type

from dateutil import parser as dateparser

from basilisp.lang import atom as atom

_DOUBLE_DOT = ".."
Expand Down Expand Up @@ -259,7 +257,7 @@ def fraction(numerator: int, denominator: int) -> Fraction:

def inst_from_str(inst_str: str) -> datetime.datetime:
"""Create a datetime instance from an RFC 3339 formatted date string."""
return dateparser.parse(inst_str)
return datetime.datetime.fromisoformat(inst_str)


def regex_from_str(regex_str: str) -> Pattern:
Expand Down
Loading
Loading