Skip to content

Commit

Permalink
Add dynamic vars for configuring the print middleware at the REPL
Browse files Browse the repository at this point in the history
  • Loading branch information
cichli committed Jan 26, 2019
1 parent aaaed72 commit 7ff1679
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 41 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
* Restore the `nrepl.bencode` namespace.
* [#117](https://github.com/nrepl/nrepl/issues/117): Replace
`nrepl.middleware.pr-values` with `nrepl.middleware.print`.
* New dynamic vars in `nrepl.middleware.print` for configuring the print
middleware at the REPL. See the Misc page in the Usage section of the
documentation for more information.
* The new middleware provides behaviour that is backwards-compatible with the
old one. Existing middleware descriptors whose `:requires` set contains
`#'pr-values` should instead use `#'wrap-print`. See the Middleware page in
Expand Down
12 changes: 9 additions & 3 deletions doc/modules/ROOT/pages/design/middleware.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ considered experimental.
nREPL includes a `print` middleware to print the results of evaluated forms as
strings for returning to the client. This enables using libraries like
link:https://github.com/greglook/puget[puget] to pretty print the evaluation
results automatically. The middleware supports the following options:
results automatically. The middleware options may be provided in either requests
or responses (the former taking precedence over the latter if any options are
specified in both). The following options are supported:

* `:nrepl.middleware.print/print`: a fully-qualified symbol naming a var whose
function to use for printing. Defaults to the equivalent of `clojure.core/pr`.
Expand Down Expand Up @@ -237,6 +239,10 @@ results automatically. The middleware supports the following options:
include `:nrepl.middleware.print/truncated-keys`, indicating which keys in
the response were truncated.

* `:nrepl.middleware.print/keys`: a seq of the keys in the response whose values
should be printed. Defaults to `[:value]` for `eval` and `load-file`
responses.

[source,clojure]
----
{:op :eval
Expand All @@ -252,8 +258,8 @@ The functionality of the `print` middleware is reusable by other middlewares. If
a middleware descriptor's `:requires` set contains
`#'nrepl.middleware.print/wrap-print`, then it can expect:

* Any responses it returns which contain the key `:nrepl.middleware.print/keys`
will have the values corresponding to those keys printed.
* Any responses it returns will have its values printed according to the above
options, as provided in the request and/or response.

** For example, to ensure that `:value` is printed, responses from the `eval`
middleware look like this:
Expand Down
72 changes: 72 additions & 0 deletions doc/modules/ROOT/pages/usage/misc.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,77 @@
= Misc Functionality

== Pretty Printing

NOTE: Pretty printing support was added in nREPL 0.6 and the API is still
considered experimental.

The namespace `nrepl.middleware.print` contains some dynamic vars you can `set!`
at the REPL to alter how evaluation results will be printed. Note that if your
nREPL client supports passing these options in requests, then it may override
some or all of these options.

* `\*print-fn*`: the function to use for printing. Defaults to the equivalent of
`clojure.core/pr`. The function must take two arguments:

** `value`: the value to print.
** `writer`: the `java.io.Writer` to print on.

* `\*stream?*`: if logical true, the result of printing each value will be
streamed to the client over one or more messages. Defaults to false.

** This lets you see results being printed incrementally, and optionally
interrupt the evaluation while it is printing.

** Your nREPL client may not fully support this mode of operation.

* `\*buffer-size*`: size of the buffer to use when streaming results. Defaults
to 1024.

* `\*quota*`: a hard limit on the number of bytes printed for each value.
Defaults to nil (no limit).

** Your nREPL client may not indicate if truncation has occurred.

For example, to prevent printing infinite lazy sequences from causing the REPL
to hang:

[source,clojure]
----
user=> (set! nrepl.middleware.print/*quota* 32)
32
user=> (range)
(0 1 2 3 4 5 6 7 8 9 10 11 12 13
user=>
----

Or to use `clojure.pprint` to print evaluation results:

[source,clojure]
----
user=> (set! nrepl.middleware.print/*print-fn* clojure.pprint/pprint)
#object[clojure.pprint$pprint 0x2bc11aa0 "clojure.pprint$pprint@2bc11aa0"]
user=> (meta #'int)
{:added "1.0",
:ns #object[clojure.lang.Namespace 0xea12515 "clojure.core"],
:name int,
:file "clojure/core.clj",
:column 1,
:line 882,
:arglists ([x]),
:doc "Coerce to int",
:inline
#object[clojure.core$int__inliner__5509 0x3c79d89 "clojure.core$int__inliner__5509@3c79d89"]}
user=>
----

However, note well that `clojure.pprint/pprint` rebinds `\*out*` internally, and
so if anything else prints to `\*out*` while evaluating, that output will end up
intermingled in the result. See the
<<../design/middleware#_pretty_printing,print middleware documentation>> for a
more detailed explanation.

== Hot-loading dependencies

From time to time you'd want to experiment with some library without
Expand Down
20 changes: 10 additions & 10 deletions src/clojure/nrepl/middleware/interruptible_eval.clj
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,17 @@
(t/send transport (response-for msg {:status #{:error :namespace-not-found :done}
:ns ns}))
(let [ctxcl (.getContextClassLoader (Thread/currentThread))
msg (-> msg
;; TODO: out-limit -> out-buffer-size | err-buffer-size
(assoc ::print/buffer-size (or out-limit (get (meta session) :out-limit)))
;; TODO: new options: out-quota | err-quota
(dissoc ::print/quota))
out (print/replying-PrintWriter :out msg)
err (print/replying-PrintWriter :err msg)]
;; TODO: out-limit -> out-buffer-size | err-buffer-size
;; TODO: new options: out-quota | err-quota
opts {::print/buffer-size (or out-limit (get (meta session) :out-limit))}
out (print/replying-PrintWriter :out msg opts)
err (print/replying-PrintWriter :err msg opts)]
(try
(clojure.main/repl
:eval (if eval (find-var (symbol eval)) clojure.core/eval)
:init #(let [bindings
(-> (get-thread-bindings)
(into print/default-bindings)
(into @session)
(into {#'*out* out
#'*err* err
Expand All @@ -96,9 +95,10 @@
;; *out* has :tag metadata; *err* does not
(.flush ^Writer *err*)
(.flush *out*)
(t/send transport (response-for msg {:ns (str (ns-name *ns*))
:value value
::print/keys #{:value}})))
(t/send transport (response-for msg (merge (print/bound-configuration)
{:ns (str (ns-name *ns*))
:value value
::print/keys #{:value}}))))
;; TODO: customizable exception prints
:caught (fn [^Throwable e]
(let [root-ex (#'clojure.main/root-cause e)
Expand Down
121 changes: 94 additions & 27 deletions src/clojure/nrepl/middleware/print.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,59 @@
(nrepl QuotaExceeded)
(nrepl.transport Transport)))

;; private in clojure.core
(defn- pr-on
[x w]
(if *print-dup*
(print-dup x w)
(print-method x w))
nil)

;; Note well that the below dynamic vars only exist for the convenience of being
;; able to set them at the REPL. They are not used directly by the middleware,
;; which only inspects requests and responses for the printing configuration.
;; The eval middleware uses `bound-configuration` to return any bound values in
;; the session in its responses.

(def ^:dynamic *print-fn*
"Function to use for printing. Takes two arguments: `value`, the value to print,
and `writer`, the `java.io.PrintWriter` to print on.
Defaults to the equivalent of `clojure.core/pr`."
pr-on)

(def ^:dynamic *stream?*
"If logical true, the result of printing each value will be streamed to the
client over one or more messages. Defaults to false."
false)

(def ^:dynamic *buffer-size*
"The size of the buffer to use when streaming results. Defaults to 1024."
1024)

(def ^:dynamic *quota*
"A hard limit on the number of bytes printed for each value. Defaults to nil. No
limit will be used if not set."
nil)

(def default-bindings
{#'*print-fn* *print-fn*
#'*stream?* *stream?*
#'*buffer-size* *buffer-size*
#'*quota* *quota*})

(defn bound-configuration
"Returns a map, suitable for merging into responses handled by this middleware,
of the currently-bound dynamic vars used for configuration."
[]
{::print-fn *print-fn*
::stream? *stream?*
::buffer-size *buffer-size*
::quota *quota*})

(def configuration-keys
[::print-fn ::stream? ::buffer-size ::quota ::keys])

(defn- to-char-array
^chars
[x]
Expand Down Expand Up @@ -71,36 +124,30 @@
(with-quota-writer quota)
(PrintWriter. true)))

;; private in clojure.core
(defn- pr-on
[x w _]
(if *print-dup*
(print-dup x w)
(print-method x w))
nil)

(defn- send-streamed
[{:keys [::print-fn transport] :as msg}
{:keys [::keys] :as resp}]
[{:keys [transport] :as msg}
resp
{:keys [::print-fn ::keys] :as opts}]
(let [print-key (fn [key]
(let [value (get resp key)]
(try
(with-open [writer (replying-PrintWriter key msg)]
(with-open [writer (replying-PrintWriter key msg opts)]
(print-fn value writer))
(catch QuotaExceeded _
(transport/send
transport
(misc/response-for msg :status ::truncated))))))]
(run! print-key keys))
(transport/send transport (apply dissoc resp (conj keys ::keys))))
(transport/send transport (apply dissoc resp keys)))

(defn- send-nonstreamed
[{:keys [::print-fn transport] :as msg}
{:keys [::keys] :as resp}]
[{:keys [transport] :as msg}
resp
{:keys [::print-fn ::quota ::keys] :as opts}]
(let [print-key (fn [key]
(let [value (get resp key)
writer (-> (StringWriter.)
(with-quota-writer msg))
(with-quota-writer quota))
truncated? (volatile! false)]
(try
(print-fn value writer)
Expand All @@ -112,21 +159,24 @@
(cond-> (assoc resp key printed-value)
truncated? (update ::truncated-keys (fnil conj []) key))))
resp (transduce (map print-key) rf resp keys)]
(transport/send transport (cond-> (dissoc resp ::keys)
(transport/send transport (cond-> resp
(::truncated-keys resp)
(update :status conj ::truncated)))))

(defn- printing-transport
[{:keys [transport ::stream?] :as msg}]
[{:keys [transport] :as msg} opts]
(reify Transport
(recv [this]
(transport/recv transport))
(recv [this timeout]
(transport/recv transport timeout))
(send [this resp]
(if stream?
(send-streamed msg resp)
(send-nonstreamed msg resp))
(let [{:keys [::stream?] :as opts} (-> (merge msg resp opts)
(select-keys configuration-keys))
resp (apply dissoc resp configuration-keys)]
(if stream?
(send-streamed msg resp opts)
(send-nonstreamed msg resp opts)))
this)))

(defn- resolve-print
Expand All @@ -152,27 +202,44 @@
Supports the following options:
* `::print` – a fully-qualified symbol naming a var whose function to use for
printing. Defaults to the equivalent of `clojure.core/pr`. Must point to a
function with signature [value writer options].
printing. Must point to a function with signature [value writer options].
* `::options` – a map of options to pass to the printing function. Defaults to
`nil`.
* `::print-fn` – the function to use for printing. In requests, will be
resolved from the above two options (if provided). Defaults to the equivalent
of `clojure.core/pr`. Must have signature [writer options].
* `::stream?` – if logical true, the result of printing each value will be
streamed to the client over one or more messages.
* `::buffer-size` – the size of the buffer to use when streaming results.
Defaults to 1024.
* `::quota` – a hard limit on the number of bytes printed for each value."
* `::quota` – a hard limit on the number of bytes printed for each value.
* `::keys` – a seq of the keys in the response whose values should be printed.
The options may be specified in either the request or the responses sent on
its transport. If any options are specified in both, those in the request will
be preferred."
[handler]
(fn [{:keys [::options] :as msg}]
(let [print-var (or (resolve-print msg) #'pr-on)
(let [print-var (resolve-print msg)
print (fn [value writer]
(print-var value writer options))
(if print-var
(print-var value writer options)
(pr-on value writer)))
msg (assoc msg ::print-fn print)
transport (printing-transport msg)]
(handler (assoc msg :transport transport)))))
opts (cond-> (select-keys msg configuration-keys)
;; no print-fn provided in the request, so defer to the response
(not print-var)
(dissoc ::print-fn)
;; in bencode empty list is logical false
(contains? msg ::stream?)
(update ::stream? #(if (= [] %) false (boolean %))))]
(handler (assoc msg :transport (printing-transport msg opts))))))

(set-descriptor! #'wrap-print {:requires #{}
:expects #{}
Expand Down
Loading

0 comments on commit 7ff1679

Please sign in to comment.