Hello! I'm having some trouble writing some middleware for nREPL. I'm trying to write a proper bit of middleware to enable org.clj-commons/pretty to override the default output of caught exceptions.
What I'm finding is that (set! *caught-fn* repl/pretty-repl-caught) isn't working.
(ns clj-commons.pretty.nrepl
"Middleware to setup pretty exception reporting in nREPL."
{:added "3.5.0"}
(:require [clj-commons.pretty.repl :as repl]
[nrepl.middleware :as middleware]
[nrepl.middleware.caught :as caught]))
(defn wrap-pretty
"Ensures that exceptions are reported using pretty-repl-caught."
[handler]
(repl/install-pretty-exceptions)
(prn :invoked `wrap-pretty)
; (alter-var-root #'caught/*caught-fn* (constantly repl/pretty-repl-caught))
(fn with-pretty [msg]
;; Outer loop binds *caught-fn* to its default (clojure.main/repl-caught) for each request.
;; This update that per-thread binding before continuing to real handler.
;; This ensures it has the correct value when control returns to nrepl.middleware.caught/wrap-caught.
(when (thread-bound? #'caught/*caught-fn*)
(set! caught/*caught-fn* repl/pretty-repl-caught))
(handler msg)))
(middleware/set-descriptor! #'wrap-pretty
{:doc (-> #'wrap-pretty meta :doc)
:handles {}
:requires #{#'caught/wrap-caught}
:expects #{"eval"}})
What I'm seeing with this is that the default binding for *caught-fn*, clojure.main/repl-caught, refuses to be displaced. I had to put that wrapper around the call to set! because the code is generally receiving the the root binding for *caught-fn*.
$ DEBUG=T lein repl
Leiningen's classpath: /opt/homebrew/Cellar/leiningen/2.11.2/libexec/leiningen-2.11.2-standalone.jar
Applying task repl to []
Applying task javac to nil
Running javac with [@/var/folders/yg/vytvxpw500520vzjlc899dlm0000gn/T/.leiningen-cmdline11883427421637590331.tmp]
Applying task compile to nil
All namespaces already AOT compiled.
:invoked clj-commons.pretty.nrepl/wrap-pretty
nREPL server started on port 51183 on host 127.0.0.1 -
:thread-bound? false
:thread-bound? false
REPL-y 0.5.1, nREPL 1.0.0
Clojure 1.12.1
OpenJDK 64-Bit Server VM 24.0.1+9-30
Docs: (doc function-name-here)
(find-doc "part-of-name-here")
Source: (source function-name-here)
Javadoc: (javadoc java-object-or-class-here)
Exit: Control+D or (exit) or (quit)
Results: Stored in vars *1, *2, *3, an exception in *e
:thread-bound? false
user=> (first 1)
:thread-bound? false
Execution error (IllegalArgumentException) at user/eval3017 (form-init10014712483504165817.clj:1).
Don't know how to create ISeq from: java.lang.Long
user=>
So that's clojure.main/repl-caught doing the output, not pretty.
I've tried swapping :requires and :exports in the middleware descriptor incase I have the logic backwards, but I think I have the order correct:
user=> (some-> nrepl.middleware.dynamic-loader/*state* deref :stack)
(#'nrepl.middleware/wrap-describe #'nrepl.middleware.interruptible-eval/interruptible-eval #'nrepl.middleware.load-file/wrap-load-file #'clj-commons.pretty.nrepl/wrap-pretty #'nrepl.middleware.caught/wrap-caught #'nrepl.middleware.print/wrap-print #'nrepl.middleware.sideloader/wrap-sideloader #'nrepl.middleware.dynamic-loader/wrap-dynamic-loader #'nrepl.middleware.session/add-stdin #'nrepl.middleware.lookup/wrap-lookup #'nrepl.middleware.completion/wrap-completion #'nrepl.middleware.session/session)
rubber-duck I have got it working correctly, but I think there's some bugs in nREPL. Details shortly.
(hazy memory but each invocation of nrepl is a new clojure.main/repl and the bindings don’t survive. i think there are tickets about printing namespace maps, metadata and print widths)
From what I can tell, attempting to bind *caught-fn* using set!` is hopeless, as:
• the bound value is not ever used outside of creating a default binding used ultimately by nrepl.middleware.session
• in theory, should be able to (set! *caught-fn* ...) to create the value that is passed to caught-transport, but if the caught key is nil, the caught-fn is removed (is this a bug?) Ah, all this merging is getting confusing, but we mege back in bound-configuraton (which is a function for some reason) and get caught-fn back BUT see all the notes about bindings.
• So what works is to bind the caught to a symbol.
(ns clj-commons.pretty.nrepl
"Middleware to setup pretty exception reporting in nREPL."
{:added "3.5.0"}
(:require [clj-commons.pretty.repl :as repl]
[nrepl.middleware :as middleware]
[nrepl.middleware.caught :as caught]))
(defn wrap-pretty
[handler]
(repl/install-pretty-exceptions)
(fn with-pretty
[msg]
(handler (assoc msg ::caught/caught `repl/pretty-repl-caught))))
(middleware/set-descriptor! #'wrap-pretty
{:doc (-> #'wrap-pretty meta :doc)
:handles {}
:requires #{}
:expects #{"eval" #'caught/wrap-caught}})
So run before wrap-caught to tell it what symbol to resolve to find the right caught-fn. Perhaps could accomplish same with a binding. Let me try that next.Nope, binding doesn't work. I'd like to figure out why not.
Not figuring it out ... but at least I have something that works.