Skip to content

Commit deb6781

Browse files
authored
Add support for taps (#631)
* Combine all imports in basilisp.core * More import statement fixes * Add support for taps * Spell better * Tests * Oop * Add some random other functions I just found * Fix weird test runner bug * Yes * More test cases
1 parent 17a64ae commit deb6781

File tree

4 files changed

+202
-58
lines changed

4 files changed

+202
-58
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
### Added
99
* Added a bootstrapping function for easily bootstrapping Basilisp projects from Python (#620)
1010
* Added support for watchers and validator functions on Atoms and Vars (#627)
11+
* Added support for Taps (#631)
1112

1213
### Changed
1314
* PyTest is now an optional extra dependency, rather than a required dependency (#622)
15+
* Generated Python functions corresponding to nested functions are now prefixed with the containing function name, if one exists (#632)
16+
17+
### Fixed
18+
* Fixed a bug where `seq`ing co-recursive lazy sequences would cause a stack overflow (#632)
19+
* Fixed a spurious failure in the test runner and switched to using macro forms for test line numbers (#631)
1420

1521
### Removed
1622
* Removed Click as a dependency in favor of builtin `argparse` (#622, #624)

src/basilisp/core.lpy

Lines changed: 124 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
(in-ns 'basilisp.core)
22

3+
(import* abc
4+
atexit
5+
collections
6+
decimal
7+
fractions
8+
functools
9+
multiprocessing
10+
os
11+
pathlib
12+
[queue :as py-queue]
13+
random
14+
re
15+
threading
16+
[time :as py-time]
17+
uuid)
18+
19+
(import* attr)
20+
321
(def ^{:doc "Create a list from the arguments."
422
:arglists '([& args])}
523
list
@@ -928,8 +946,6 @@
928946
;; Futures ;;
929947
;;;;;;;;;;;;;
930948

931-
(import* atexit)
932-
933949
(def ^:dynamic *executor-pool*
934950
(basilisp.lang.futures/ThreadPoolExecutor))
935951

@@ -1103,8 +1119,6 @@
11031119
;; Simple Predicates ;;
11041120
;;;;;;;;;;;;;;;;;;;;;;;
11051121

1106-
(import* decimal fractions uuid)
1107-
11081122
(defn any?
11091123
"Return true for any x."
11101124
[_]
@@ -1404,6 +1418,24 @@
14041418
;; Type Coercion ;;
14051419
;;;;;;;;;;;;;;;;;;;
14061420

1421+
(defn cast
1422+
"Throws a `TypeError` if x is not a cls. Otherwise, return x."
1423+
[cls x]
1424+
(when-not (instance? cls x)
1425+
(throw (python/TypeError
1426+
(str "Cannot cast object of type " (python/type x) " to " cls))))
1427+
x)
1428+
1429+
(defn class
1430+
"Return the class of x."
1431+
[x]
1432+
(python/type x))
1433+
1434+
(defn type
1435+
"Return the type of x."
1436+
[x]
1437+
(python/type x))
1438+
14071439
(defn bigdec
14081440
"Coerce x to a Decimal."
14091441
[x]
@@ -3134,8 +3166,6 @@
31343166
(mapcat walk (children node))))))]
31353167
(walk root)))
31363168

3137-
(import* multiprocessing)
3138-
31393169
(def ^:dynamic *pmap-cpu-count*
31403170
(* 2 (multiprocessing/cpu-count)))
31413171

@@ -3190,8 +3220,6 @@
31903220
;; Random Functions ;;
31913221
;;;;;;;;;;;;;;;;;;;;;;
31923222

3193-
(import* random)
3194-
31953223
(defn rand
31963224
"Return a random real number between lower (default: 0) and upper (default: 1) inclusive."
31973225
([] (random/uniform 0 1))
@@ -3627,8 +3655,6 @@
36273655
(finally
36283656
(pop-thread-bindings)))))
36293657

3630-
(import* [time :as py-time])
3631-
36323658
(defn ^:private perf-counter
36333659
[]
36343660
(py-time/perf-counter))
@@ -3750,8 +3776,6 @@
37503776
;; String Functions ;;
37513777
;;;;;;;;;;;;;;;;;;;;;;
37523778

3753-
(import* os)
3754-
37553779
(defn format
37563780
"Format a string as by Python's `%` operator."
37573781
[fmt & args]
@@ -3781,8 +3805,6 @@
37813805
;; File Functions ;;
37823806
;;;;;;;;;;;;;;;;;;;;
37833807

3784-
(import* pathlib)
3785-
37863808
(defn file-seq
37873809
"Return a seq of `pathlib.Path` objects for all files and subdirectories of `dir`."
37883810
[dir]
@@ -4520,8 +4542,6 @@
45204542
;; Regex Functions ;;
45214543
;;;;;;;;;;;;;;;;;;;;;
45224544

4523-
(import re)
4524-
45254545
(defn re-pattern
45264546
"Return a new re.Pattern instance."
45274547
[s]
@@ -4577,7 +4597,7 @@
45774597
;; Multimethods ;;
45784598
;;;;;;;;;;;;;;;;;;
45794599

4580-
(import basilisp.lang.multifn)
4600+
(import* basilisp.lang.multifn)
45814601

45824602
(defmacro defmulti
45834603
"Define a new multimethod with the dispatch function."
@@ -5034,8 +5054,6 @@
50345054
;; Interfaces ;;
50355055
;;;;;;;;;;;;;;;;
50365056

5037-
(import* abc)
5038-
50395057
(defmulti munge
50405058
"Munge the input value into a Python-safe string. Converts keywords and
50415059
symbols into strings as by `name` prior to munging. Returns a string."
@@ -5174,8 +5192,6 @@
51745192
;; Protocols ;;
51755193
;;;;;;;;;;;;;;;
51765194

5177-
(import* functools)
5178-
51795195
(defn ^:private gen-protocol-dispatch
51805196
"Return the dispatch function for a single protocol method."
51815197
[protocol-name interface-name [method-name & args+docstring :as method-def]]
@@ -5656,8 +5672,6 @@
56565672
;; Records ;;
56575673
;;;;;;;;;;;;;
56585674

5659-
(import* attr)
5660-
56615675
(defn record?
56625676
"Return true if v is a record type."
56635677
[v]
@@ -5937,8 +5951,6 @@
59375951
;; Transducers ;;
59385952
;;;;;;;;;;;;;;;;;
59395953

5940-
(import* collections)
5941-
59425954
;; Eduction types return a custom iterator type which continually pulls from the
59435955
;; input collection (cast as a `seq`). `xf` is called on each value of that
59445956
;; sequence and it stores eligible values in a queue (`buf`). `__next__` adds
@@ -6125,3 +6137,90 @@
61256137
([result] (rf result))
61266138
([result input]
61276139
(reduce rf result input))))
6140+
6141+
;;;;;;;;;;
6142+
;; Taps ;;
6143+
;;;;;;;;;;
6144+
6145+
(defonce ^:private tap-queue
6146+
(->> (os/getenv "BASILISP_TAP_QUEUE_SIZE" 1024)
6147+
(python/int)
6148+
(py-queue/Queue)))
6149+
6150+
(defonce ^:private tapset (atom {}))
6151+
6152+
(defonce ^:private tap-thread
6153+
(delay
6154+
(doto (threading/Thread
6155+
**
6156+
:target (fn []
6157+
(loop []
6158+
(let [{:keys [topic val]} (.get tap-queue)
6159+
topic-tapset (get @tapset topic #{})]
6160+
(doseq [tap (seq topic-tapset)]
6161+
(try
6162+
(tap val)
6163+
(catch python/Exception _ nil)))
6164+
(.task-done tap-queue))
6165+
(recur)))
6166+
:daemon true)
6167+
(.start))))
6168+
6169+
(defn add-tap
6170+
"Add `tf`, a function of one argument, to the tap set for the topic `topic`. If no
6171+
topic is given, the default topic is used.
6172+
6173+
Tap functions are called only with values from calls to `tap>` with the topic they
6174+
are added with.
6175+
6176+
Taps may be removed from the tap set for a topic (or with the default topic) by a
6177+
matching call to `remove-tap`. Taps are identified only by their identity, so hang
6178+
on to a reference if you do need to remove the tap.
6179+
6180+
Returns `nil`."
6181+
([tf]
6182+
(add-tap :basilisp.core.tap/default tf))
6183+
([topic tf]
6184+
(force tap-thread)
6185+
(swap! tapset
6186+
(fn [old-state]
6187+
(let [topic-tapset (get old-state topic #{})]
6188+
(->> (conj topic-tapset tf)
6189+
(assoc old-state topic)))))
6190+
nil))
6191+
6192+
(defn remove-tap
6193+
"Remove a tap function from the tap set added by `add-tap`.
6194+
6195+
Tap functions may only be removed from the tap set corresponding to the topic they
6196+
were added with. If no topic is given, the default topic is used.
6197+
6198+
Returns `nil` in all cases."
6199+
([tf]
6200+
(remove-tap :basilisp.core.tap/default tf))
6201+
([topic tf]
6202+
(force tap-thread)
6203+
(swap! tapset
6204+
(fn [old-state]
6205+
(let [topic-tapset (get old-state topic #{})]
6206+
(->> (disj topic-tapset tf)
6207+
(assoc old-state topic)))))
6208+
nil))
6209+
6210+
(defn tap>
6211+
"Send the value `val` to all tap functions registered for the topic. If no topic
6212+
is given, the default topic is used.
6213+
6214+
`tap>` will never block, though if the tap queue is full then tap values may be
6215+
dropped.
6216+
6217+
Returns true if `val` was sent to the queue, `false` if `val` was dropped."
6218+
([val]
6219+
(tap> :basilisp.core.tap/default val))
6220+
([topic val]
6221+
(force tap-thread)
6222+
(try
6223+
(do
6224+
(.put-nowait tap-queue {:topic topic :val val})
6225+
true)
6226+
(catch queue/Full _ false))))

src/basilisp/test.lpy

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
(ns basilisp.test
2-
(:import
3-
inspect)
42
(:require
53
[basilisp.template :as template]))
64

@@ -11,24 +9,14 @@
119
(def ^:dynamic *test-section* nil)
1210
(def ^:dynamic *test-failures* nil)
1311

14-
(defn line-no
15-
"Get the line number from the current interpreter stack.
16-
17-
This is a horrible hack and it requires each particular assertion case
18-
to define their frame offset from here, but it seems to be the most
19-
accessible way of determining the line number without capturing it
20-
for every test (including non-failing tests)."
21-
[n]
22-
(.-lineno (nth (inspect/stack) n)))
23-
2412
(defmulti gen-assert
25-
(fn [expr _]
13+
(fn [expr _ _]
2614
(cond
2715
(list? expr) (first expr)
2816
:else :default)))
2917

3018
(defmethod gen-assert '=
31-
[expr msg]
19+
[expr msg line-num]
3220
`(when-not ~expr
3321
(swap! *test-failures*
3422
conj
@@ -38,11 +26,11 @@
3826
:expr (quote ~expr)
3927
:actual ~(nth expr 2)
4028
:expected ~(second expr)
41-
:line (line-no 1)
29+
:line ~line-num
4230
:type :failure})))
4331

4432
(defmethod gen-assert 'thrown?
45-
[expr msg]
33+
[expr msg line-num]
4634
(let [exc-type (second expr)
4735
body (nthnext expr 2)]
4836
`(try
@@ -55,7 +43,7 @@
5543
:expr (quote ~expr)
5644
:actual result#
5745
:expected (quote ~exc-type)
58-
:line (line-no 1)
46+
:line ~line-num
5947
:type :failure}))
6048
(catch ~exc-type _ nil)
6149
(catch python/Exception e#
@@ -67,11 +55,11 @@
6755
:expr (quote ~expr)
6856
:actual e#
6957
:expected ~exc-type
70-
:line (line-no 1)
58+
:line ~line-num
7159
:type :failure})))))
7260

7361
(defmethod gen-assert :default
74-
[expr msg]
62+
[expr msg line-num]
7563
`(let [computed# ~expr]
7664
(when-not computed#
7765
(swap! *test-failures*
@@ -82,27 +70,28 @@
8270
:expr (quote ~expr)
8371
:actual computed#
8472
:expected computed#
85-
:line (line-no 1)
73+
:line ~line-num
8674
:type :failure}))))
8775

8876
(defmacro is
8977
"Assert that expr is true. Must appear inside of a deftest form."
9078
([expr]
9179
`(is ~expr (str "Test failure: " (pr-str (quote ~expr)))))
9280
([expr msg]
93-
`(try
94-
~(gen-assert expr msg)
95-
(catch python/Exception e#
96-
(swap! *test-failures*
97-
conj
98-
{:test-name *test-name*
99-
:test-section *test-section*
100-
:message (str "Unexpected exception thrown during test run: " (python/repr e#))
101-
:expr (quote ~expr)
102-
:actual e#
103-
:expected (quote ~expr)
104-
:line (line-no 1)
105-
:type :error})))))
81+
(let [line-num (:basilisp.lang.reader/line (meta &form))]
82+
`(try
83+
~(gen-assert expr msg line-num)
84+
(catch python/Exception e#
85+
(swap! *test-failures*
86+
conj
87+
{:test-name *test-name*
88+
:test-section *test-section*
89+
:message (str "Unexpected exception thrown during test run: " (python/repr e#))
90+
:expr (quote ~expr)
91+
:actual e#
92+
:expected (quote ~expr)
93+
:line ~line-num
94+
:type :error}))))))
10695

10796
(defmacro are
10897
"Assert that expr is true. Must appear inside of a deftest form."

0 commit comments

Comments
 (0)