Please help me pprint code docstrings


I’m printing some code and this is the best I could come up with:

(defn pprint-str [x]
  (-> (pprint/pprint x)
      (->> (pprint/with-pprint-dispatch pprint/code-dispatch))
      (str/replace "\\n" "\n")))

I pass in (quoted) forms like:

(defn f "hi\nthere" [x] (inc x))

As you can see, the docstring has a newline in it, which pprint escapes (as edn) but I don’t want that because it is code. This code gets written out as a namespace and it looks wrong to have the docstrings contain escaped newlines. So as a workaround I then replace all escaped newlines with normal newlines. This is not perfect though, because functions could include strings in their body, and those should remain escaped.

It seems like a bit of a challenge to differentiate docstrings from other strings and treat them differently… I looked into cljfmt and rewrite-clj which handle this problem just fine, but I couldn’t figure out how they actually do it .

Does anyone have any suggestions on how to solve the issue with printing docstrings, or can point me to how cljfmt solves it?

I don’t see any escaping?..

user=> (pprint-str '(defn f "hi\nthere" [x] (inc x)))
"(defn f \"hi\nthere\" [x] (inc x))\n"

user=> (println *1)
(defn f "hi
there" [x] (inc x))

1 Like

@p-himik I think the issue was that the (str/replace "\\n" "\n") was not wanted in general—it was the desired behavior for docstrings, but not all the other strings.

Short answer: it appears that clojure.pprint/code-dispatch is built in a way that is not easily configurable.

Longer answer: clojure.pprint/code-dispatch uses clojure.pprint/*code-table*, a private dynamic var. *code-table* is a map from symbol to function printing that symbol. Its entry for 'defn is clojure.pprint/pprint-defn, also private.

Even longer, derailing answer: It appears that we can monkey-patch clojure.pprint/*code-table* with our own defn printer:

(ns scratch
   [clojure.pprint :as pprint]

(def my-function-code
  '(defn f "hi\nthere" [x] (inc x)))

(pprint/with-pprint-dispatch pprint/code-dispatch
  (pprint/pprint my-function-code))
;; Prints like Tim showed.

(def original-code-table (deref #'clojure.pprint/*code-table*))

(defn teodor-defn-writer [_]
  (println "(defn teodor-function [])"))

(binding [clojure.pprint/*code-table* (assoc original-code-table 'defn teodor-defn-writer)]
  (pprint/with-pprint-dispatch pprint/code-dispatch
    (pprint/pprint my-function-code)))
;; Prints "(defn teodor-function [])"

I tried reading the original implementation for printing code, but I couldn’t understand what was going on. Here it is:

I think it might be easier to print to a string first, then use rewrite-clj to traverse over the resulting string, changing only the docstring in defns to what you want.

Which is more in line with both what you imply and what @p-himik suggests :smile:

Ah, duh. Copied the code blindly and completely missed that str/replace is already in there.

@timothypratley cljfmt, as far as I can tell, also doesn’t work with that form. It preserves that \n:

user=> (require '[cljfmt.core :as fmt] '[rewrite-clj.node :as n])
user=> (println (n/string (fmt/reformat-form '(defn f "hi\nthere" [x] (inc x)))))
(defn f "hi\nthere" [x] (inc x))
user=> (println (fmt/reformat-string "(defn f \"hi\\nthere\" [x] (inc x))"))
(defn f "hi\nthere" [x] (inc x))

Of course it also doesn’t work if you use a form and use a literal newline, since it’s not different from \n:

user=> (println (n/string (fmt/reformat-form '(defn f "hi
there" [x] (inc x)))))
(defn f "hi\nthere" [x] (inc x))

But of course it does work if that \n is not escaped (well, in a way):

user=> (println (fmt/reformat-string "(defn f \"hi\nthere\" [x] (inc x))"))
(defn f "hi
there" [x] (inc x))

So that makes me think that formatting is done not on forms but on strings.
And yeah, here it is in the sources: cljfmt/cljfmt/src/cljfmt/tool.clj at master · weavejester/cljfmt · GitHub

1 Like

Oh I see now, thank you @p-himik and @teodorlu that explains it perfectly.
Much appreciated!

1 Like