From 1616e09b07624f66bdf211f2fa452d65b2e02d14 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Fri, 16 May 2025 16:35:02 -0700 Subject: [PATCH 01/15] Initial basilisp runtime support --- .github/workflows/clojure.yml | 27 +- .gitignore | 2 + .vscode/settings.json | 17 + bb.edn | 1 + dev/notebook/ci.clj | 1 + dev/tasks/bench.clj | 3 +- dev/tasks/py.clj | 18 + dev/tasks/test.clj | 5 + dev/tasks/tools.clj | 11 + dev/user.clj | 1 + examples/clr/.gitignore | 1 + requirements.txt | 16 + resources/runtime/python.svg | 69 ++ src/portal/api.cljc | 8 +- src/portal/client/py.lpy | 14 +- src/portal/console.cljc | 24 +- src/portal/runtime.cljc | 79 ++- src/portal/runtime/browser.cljc | 10 +- src/portal/runtime/cson.cljc | 636 ++++++++++++------ src/portal/runtime/fs.cljc | 59 +- src/portal/runtime/index.cljc | 2 +- src/portal/runtime/json.cljc | 11 +- src/portal/runtime/json_buffer.cljc | 47 +- src/portal/runtime/protocols.cljc | 13 + src/portal/runtime/python/client.lpy | 99 +++ src/portal/runtime/python/launcher.lpy | 151 +++++ src/portal/runtime/python/server.lpy | 91 +++ src/portal/runtime/rpc.cljc | 20 +- src/portal/runtime/shell.cljc | 12 +- src/portal/runtime/transit.cljc | 4 + src/portal/sync.cljc | 2 +- src/portal/ui/inspector.cljs | 32 +- src/portal/ui/rpc.cljs | 4 +- src/portal/ui/rpc/runtime.cljs | 6 +- src/portal/ui/viewer/diff.cljs | 6 +- src/portal/ui/viewer/log.cljs | 5 +- src/portal/viewer.cljc | 5 +- {src => test}/examples/data.cljc | 0 {src => test}/examples/default_visualizer.clj | 0 {src => test}/examples/fetch.cljs | 0 {src => test}/examples/hacker_news.cljc | 0 {src => test}/examples/macros.cljc | 0 test/portal/bench.cljc | 11 +- test/portal/client_test.cljc | 7 + .../runtime/{api_test.clj => api_test.cljc} | 19 +- test/portal/runtime/bench_cson.cljc | 4 +- test/portal/runtime/cson_test.cljc | 56 +- test/portal/runtime/npm_test.cljc | 2 +- test/portal/runtime_test.cljc | 2 +- test/portal/test_runner.lpy | 63 ++ 50 files changed, 1326 insertions(+), 350 deletions(-) create mode 100644 dev/tasks/py.clj create mode 100644 requirements.txt create mode 100644 resources/runtime/python.svg create mode 100644 src/portal/runtime/protocols.cljc create mode 100644 src/portal/runtime/python/client.lpy create mode 100644 src/portal/runtime/python/launcher.lpy create mode 100644 src/portal/runtime/python/server.lpy rename {src => test}/examples/data.cljc (100%) rename {src => test}/examples/default_visualizer.clj (100%) rename {src => test}/examples/fetch.cljs (100%) rename {src => test}/examples/hacker_news.cljc (100%) rename {src => test}/examples/macros.cljc (100%) rename test/portal/runtime/{api_test.clj => api_test.cljc} (69%) create mode 100644 test/portal/test_runner.lpy diff --git a/.github/workflows/clojure.yml b/.github/workflows/clojure.yml index 60c680b2..bc47ed80 100644 --- a/.github/workflows/clojure.yml +++ b/.github/workflows/clojure.yml @@ -102,9 +102,32 @@ jobs: - run: dotnet tool install --global Clojure.Main --version ${{ matrix.version }} - run: dotnet restore - run: bb -m tasks.test/cljr + test-py: + needs: [ setup, build ] + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: browser-actions/setup-chrome@v1 + with: + chrome-version: 135 + - uses: actions/checkout@v3 + - uses: ./.github/setup + - run: rm package.json + - run: npm install "react@^17.0.2" + - uses: actions/download-artifact@v4 + with: + name: portal-client + path: resources/portal + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - run: pip install virtualenv + - run: bb -m tasks.test/lpy app: runs-on: ubuntu-latest - needs: [ test-ui, test-clj, test-cljs, test-cljr, check ] + needs: [ test-ui, test-clj, test-cljs, test-cljr, test-py, check ] if: github.event_name == 'push' steps: - uses: actions/checkout@v3 @@ -117,7 +140,7 @@ jobs: force_orphan: true package: runs-on: ubuntu-latest - needs: [ test-ui, test-clj, test-cljs, test-cljr, check ] + needs: [ test-ui, test-clj, test-cljs, test-cljr, test-py, check ] if: startsWith(github.event.head_commit.message, 'Release ') steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index cd361929..801cdadc 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ clojure.data.json* *.iml obj/ + +**__pycache__** \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 78cf0d7f..df6d58bb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -90,6 +90,23 @@ ":nrepl" ] } + }, + { + "name": "basilisp", + "projectType": "deps.edn", + "projectRootPath": [ + "." + ], + "customJackInCommandLine": "bb py", + "nReplPortFile": [ + ".nrepl-port" + ], + "menuSelections": { + "cljAliases": [ + ":dev", + ":cider" + ] + } } ] } \ No newline at end of file diff --git a/bb.edn b/bb.edn index 48bea730..427f9884 100644 --- a/bb.edn +++ b/bb.edn @@ -17,4 +17,5 @@ ide tasks.ide/open pkg tasks.package/all deploy tasks.deploy/all + py tasks.py/-main bench tasks.bench/-main}} diff --git a/dev/notebook/ci.clj b/dev/notebook/ci.clj index 1f5c4db0..e7f470aa 100644 --- a/dev/notebook/ci.clj +++ b/dev/notebook/ci.clj @@ -15,6 +15,7 @@ (with-out-data (test/cljs-runtime "1.10.844")) (with-out-data (test/cljs-nbb)) (with-out-data (test/cljs-ui)) +(with-out-data (test/lpy)) (build) (with-out-data (tool/clj "-M:test" "-m" :portal.test-runner)) (with-out-data (tool/bb "-m" :portal.test-runner)) diff --git a/dev/tasks/bench.clj b/dev/tasks/bench.clj index b5940c52..5ea5052c 100644 --- a/dev/tasks/bench.clj +++ b/dev/tasks/bench.clj @@ -15,7 +15,8 @@ (for [f [#(t/clj "-M:test" "-m" :portal.runtime.bench-cson) #(t/bb "-m" :portal.runtime.bench-cson) #(t/cljr "-m" :portal.runtime.bench-cson) - #(t/nbb "-m" :portal.runtime.bench-cson)]] + #(t/nbb "-m" :portal.runtime.bench-cson) + #_#(t/lpy :run "-n" :portal.runtime.bench-cson)]] (future (with-out-data (f)))))) (def windows diff --git a/dev/tasks/py.clj b/dev/tasks/py.clj new file mode 100644 index 00000000..779cd384 --- /dev/null +++ b/dev/tasks/py.clj @@ -0,0 +1,18 @@ +(ns tasks.py + (:require [babashka.fs :as fs] + [tasks.build :refer [build]] + [tasks.tools :refer [py pip lpy]])) + +(defn install [] + (when-not (fs/exists? "target/py") + (py "-m" :venv "target/py") + (pip :install "-r" "requirements.txt"))) + +(defn nrepl [] (lpy :nrepl-server)) + +(defn -main + "Start basilisp dev env / nrepl" + [] + (build) + (install) + (nrepl)) \ No newline at end of file diff --git a/dev/tasks/test.clj b/dev/tasks/test.clj index 6198d876..75301cda 100644 --- a/dev/tasks/test.clj +++ b/dev/tasks/test.clj @@ -2,6 +2,7 @@ (:refer-clojure :exclude [test]) (:require [babashka.fs :as fs] [tasks.build :refer [build install]] + [tasks.py :as py] [tasks.tools :as t])) (defn cljs* [deps main] @@ -60,6 +61,10 @@ (build) (t/cljr "-m" :portal.test-clr)) +(defn lpy [] + (py/install) + (t/lpy :run "-n" :portal.test-runner)) + (defn test* [] (future (cljs-runtime "1.10.773")) (future (cljs-runtime "1.10.844")) diff --git a/dev/tasks/tools.clj b/dev/tasks/tools.clj index b9aaf803..b27df2fa 100644 --- a/dev/tasks/tools.clj +++ b/dev/tasks/tools.clj @@ -89,3 +89,14 @@ (str/join (System/getProperty "path.separator") ["src" "resources" "dev" "test"]))] (apply sh :Clojure.Main args))) + +(def py (partial #'sh :python3)) +(def pip (partial #'sh "./target/py/bin/pip")) + +(defn lpy [& args] + (binding [*opts* + (assoc *opts* + :inherit true + :extra-env + {"PYTHONPATH" "src:test"})] + (apply sh "./target/py/bin/basilisp" args))) diff --git a/dev/user.clj b/dev/user.clj index 827baf5f..53e83179 100644 --- a/dev/user.clj +++ b/dev/user.clj @@ -22,6 +22,7 @@ (p/clear) (p/close) + (p/stop) (p/docs {:mode :dev}) (def portal (p/open {:launcher :auto})) diff --git a/examples/clr/.gitignore b/examples/clr/.gitignore index 0916593a..636c46a6 100644 --- a/examples/clr/.gitignore +++ b/examples/clr/.gitignore @@ -1,2 +1,3 @@ .classpath +.clj-kondo/ .cpcache/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..00ec0ff7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +aiohappyeyeballs==2.4.4 +aiohttp==3.11.11 +aiosignal==1.3.2 +asyncio==3.4.3 +attrs==24.3.0 +basilisp==0.4.0 +frozenlist==1.5.0 +idna==3.10 +immutables==0.21 +multidict==6.1.0 +prompt_toolkit==3.0.48 +propcache==0.2.1 +pyrsistent==0.20.0 +typing_extensions==4.12.2 +wcwidth==0.2.13 +yarl==1.18.3 diff --git a/resources/runtime/python.svg b/resources/runtime/python.svg new file mode 100644 index 00000000..500846bd --- /dev/null +++ b/resources/runtime/python.svg @@ -0,0 +1,69 @@ + + + + + + + + image/svg+xml + + + + + + + + diff --git a/src/portal/api.cljc b/src/portal/api.cljc index 0a6b1dce..29632694 100644 --- a/src/portal/api.cljc +++ b/src/portal/api.cljc @@ -3,10 +3,12 @@ [portal.runtime.jvm.commands]) #?(:clj [portal.runtime.jvm.launcher :as l] :cljs [portal.runtime.node.launcher :as l] - :cljr [portal.runtime.clr.launcher :as l]) + :cljr [portal.runtime.clr.launcher :as l] + :lpy [portal.runtime.python.launcher :as l]) #?(:clj [portal.sync :as a] :cljs [portal.async :as a] - :cljr [portal.sync :as a]) + :cljr [portal.sync :as a] + :lpy [portal.sync :as a]) #?(:clj [clojure.java.io :as io] :cljs [portal.resources :as io]) [clojure.set :as set] @@ -44,7 +46,7 @@ :portal.launcher/window-title :window-title}) (defn- rename [options] - (set/rename-keys options long->short)) + (set/rename-keys (or options {}) long->short)) (defn set-defaults! "Set default options for `open` and `start`. diff --git a/src/portal/client/py.lpy b/src/portal/client/py.lpy index 76d988e8..a4f035b8 100644 --- a/src/portal/client/py.lpy +++ b/src/portal/client/py.lpy @@ -3,15 +3,19 @@ (:import [urllib.request :as request])) (defn- serialize [encoding value] - (.encode - (try + (try + (.encode (case encoding :json (json/write-str value) :edn (binding [*print-meta* true] (pr-str value))) - (catch Exception ex - (serialize encoding (pr-str ex)))) - "utf-8")) + "utf-8") + (catch Exception ex + (serialize + encoding + {:cause "Error" + :message (ex-message ex) + :data (ex-data ex)})))) (defn submit ([value] (submit nil value)) diff --git a/src/portal/console.cljc b/src/portal/console.cljc index 8aca308c..9e041de8 100644 --- a/src/portal/console.cljc +++ b/src/portal/console.cljc @@ -2,15 +2,22 @@ #?(:joyride (:import) :clj (:require [clojure.java.io :as io]) :portal (:import) - :cljs (:require-macros portal.console))) + :cljs (:require-macros portal.console) + :lpy (:import [datetime :as datetime]))) (defn ^:no-doc now [] - #?(:clj (java.util.Date.) :cljs (js/Date.) :cljr (DateTime/Now))) + #?(:clj (java.util.Date.) + :cljs (js/Date.) + :cljr (DateTime/Now) + :lpy (.now datetime/datetime))) (defn ^:no-doc run [f] (try [nil (f)] - (catch #?(:clj Exception :cljs :default :cljr Exception) ex + (catch #?(:clj Exception + :cljs :default + :cljr Exception + :lpy Exception) ex [:throw ex]))) (defn ^:no-doc runtime [] @@ -20,7 +27,8 @@ :bb :bb :clj :clj :cljs :cljs - :cljr :cljr)) + :cljr :cljr + :lpy :py)) #?(:clj (defn ^:no-doc get-file [env file] @@ -32,7 +40,10 @@ #_{:clj-kondo/ignore #?(:cljs [:unused-binding] :default [])} (defn ^:no-doc capture [level form expr env] - (let [{:keys [line column file]} (meta form)] + (let [#?(:lpy {line :basilisp.lang.reader/line + column :basilisp.lang.reader/col} + :default {:keys [line column file]}) + (meta form)] `(let [[flow# result#] (run (fn [] ~expr))] (tap> (with-meta @@ -45,7 +56,8 @@ :joyride '*file* :org.babashka/nbb *file* :cljs file - :cljr *file*) + :cljr *file* + :lpy "unknown") :line ~line :column ~column :time (now) diff --git a/src/portal/runtime.cljc b/src/portal/runtime.cljc index 184a25e6..68d0f8a8 100644 --- a/src/portal/runtime.cljc +++ b/src/portal/runtime.cljc @@ -2,19 +2,23 @@ (:refer-clojure :exclude [read]) (:require #?(:clj [portal.sync :as a] :cljr [portal.sync :as a] - :cljs [portal.async :as a]) + :cljs [portal.async :as a] + :lpy [portal.sync :as a]) #?(:joyride [portal.runtime.datafy :refer [datafy nav]] :org.babashka/nbb [portal.runtime.datafy :refer [datafy nav]] + :lpy [portal.runtime.datafy :refer [datafy nav]] :default [clojure.datafy :refer [datafy nav]]) #?(:joyride [cljs.pprint :as pprint] :default [clojure.pprint :as pprint]) [portal.runtime.cson :as cson] - [portal.viewer :as v])) + [portal.viewer :as v]) + #?(:lpy (:import [asyncio :as asyncio]))) (def ^:private tagged-type (type (cson/->Tagged "tag" []))) #?(:joyride nil :org.babashka/nbb nil + :lpy nil :default (defmethod pprint/simple-dispatch tagged-type [value] (if (not= (:tag value) "remote") @@ -66,15 +70,23 @@ (defonce request (atom nil)) +#?(:lpy (defonce ^:no-doc async-loop (atom nil))) + (defn- set-timeout [f ^long timeout] #?(:clj (future (Thread/sleep timeout) (f)) :cljr (future (System.Threading.Thread/Sleep timeout) (f)) - :cljs (js/setTimeout f timeout))) + :cljs (js/setTimeout f timeout) + :lpy (asyncio/run_coroutine_threadsafe + (^:async + (fn [] + (await (asyncio/sleep (/ timeout 1000.0))) + (future (f)))) + @async-loop))) (defn- hashable? [value] (try (and (hash value) true) - (catch #?(:clj Exception :cljr Exception :cljs :default) _ + (catch #?(:cljs :default :default Exception) _ false))) #?(:bb (def clojure.lang.Range (type (range 1.0)))) @@ -96,7 +108,13 @@ (try (with-meta value {}) true (catch :default _e false))) - :cljs (implements? IMeta value))) + :cljs (implements? IMeta value) + + :lpy + (try (with-meta value {}) true + (catch Exception _e false)))) + +#?(:lpy (defn- sorted? [_] false)) (defn- hash+ [x] (cond @@ -138,7 +156,8 @@ :cljr (instance? clojure.lang.Atom o) :joyride (= Atom (type o)) :org.babashka/nbb (= Atom (type o)) - :cljs (satisfies? cljs.core/IAtom o))) + :cljs (satisfies? cljs.core/IAtom o) + :lpy (instance? basilisp.lang.atom/Atom o))) (defn- notify [session-id a] (when-let [request @request] @@ -203,19 +222,20 @@ (defn- deref? [value] #?(:clj (instance? clojure.lang.IRef value) :cljr (instance? clojure.lang.IRef value) - :cljs (satisfies? cljs.core/IDeref value))) + :cljs (satisfies? cljs.core/IDeref value) + :lpy (atom? value))) (defn- pr-str' [value] (try (if-not (deref? value) (pr-str value) (str "#object " (pr-str [(type value) {:val ::elided}]))) - (catch #?(:clj Exception :cljr Exception :cljs :default) _ + (catch #?(:cljs :default :default Exception) _ (str "#object [" (pr-str (type value)) " unprintable]")))) (defn- to-object [buffer value tag rep] (if-not *session* - (cson/-to-json + (cson/to-json* (with-meta (cson/tagged-value "remote" (pr-str value)) (meta value)) @@ -238,9 +258,9 @@ :clj (extend-type java.util.Collection cson/ToJson - (-to-json [value buffer] + (to-json* [value buffer] (if-let [id (value->id? value)] - (cson/-to-json (cson/tagged-value "ref" id) buffer) + (cson/to-json* (cson/tagged-value "ref" id) buffer) (cson/tagged-coll buffer (cond @@ -254,9 +274,9 @@ :clj (extend-type java.util.Map cson/ToJson - (-to-json [value buffer] + (to-json* [value buffer] (if-let [id (value->id? value)] - (cson/-to-json (cson/tagged-value "ref" id) buffer) + (cson/to-json* (cson/tagged-value "ref" id) buffer) (cson/tagged-map buffer "{" @@ -267,15 +287,16 @@ (extend-type #?(:clj Object :cljr Object - :cljs default) + :cljs default + :lpy python/object) cson/ToJson - (-to-json [value buffer] + (to-json* [value buffer] (to-object buffer value :object nil))) (defn- has? [m k] (try (k m) - (catch #?(:clj Exception :cljr Exception :cljs :default) _e))) + (catch #?(:cljs :default :default Exception) _e nil))) (defn- no-cache [value] (or (not (coll? value)) @@ -333,6 +354,13 @@ (atom (with-meta (list) {:portal.viewer/default :portal.viewer/inspector}))) +(comment + (reset! tap-list + (with-meta (list) + {:portal.viewer/default :portal.viewer/inspector})) + (meta @tap-list) + (tap> :hi)) + (defn- realize-value! [x] (cond (map? x) @@ -345,21 +373,24 @@ #_{:clj-kondo/ignore [:unused-private-var]} (defn- runtime [] - #?(:portal :portal :bb :bb :clj :clj :joyride :joyride :org.babashka/nbb :nbb :cljs :cljs :cljr :cljr)) + #?(:portal :portal :bb :bb :clj :clj :joyride :joyride :org.babashka/nbb :nbb :cljs :cljs :cljr :cljr :lpy :py)) (defn- error->data [e] #?(:clj (assoc (Throwable->map e) :runtime (runtime)) :cljr (assoc (Throwable->map e) :runtime (runtime)) - :cljs e)) + :default e)) (defn update-value [new-value] (try (realize-value! new-value) (swap! tap-list conj new-value) - (catch #?(:clj Exception :cljr Exception :cljs :default) e + (catch #?(:cljs :default :default Exception) e (swap! tap-list conj (error->data - (ex-info "Failed to receive value." {:value-type (type new-value)} e)))))) + #?(:lpy + (ex-info "Failed to receive value." {:value-type (type new-value)}) + :default + (ex-info "Failed to receive value." {:value-type (type new-value)} e))))))) (def ^:private runtime-keymap (atom ^::no-cache {})) @@ -376,6 +407,7 @@ #?(:bb "bb" :clj "jvm" :cljr "clr" + :lpy "py" :joyride "joyride" :org.babashka/nbb "nbb" :cljs (cond @@ -435,7 +467,7 @@ (cond-> out (predicate v) (assoc name result)) - (catch #?(:clj Exception :cljr Exception :cljs :default) _ex out)) + (catch #?(:cljs :default :default Exception) _ex out)) (assoc out name result))))) {} @registry) @@ -457,7 +489,7 @@ (a/try (a/let [return (binding [*session* session] (apply f args))] (done (assoc (source-info f) :return return))) - (catch #?(:clj Exception :cljr Exception :cljs js/Error) e + (catch #?(:cljs :default :default Exception) e (done (assoc (source-info f) :error @@ -466,8 +498,7 @@ {::function f ::args args ::found? (some? f) - ::data (ex-data e)} - e) + ::data (ex-data e)}) datafy (assoc :runtime (runtime))))))))) diff --git a/src/portal/runtime/browser.cljc b/src/portal/runtime/browser.cljc index 9f23a875..d259a1b9 100644 --- a/src/portal/runtime/browser.cljc +++ b/src/portal/runtime/browser.cljc @@ -18,6 +18,12 @@ [portal.runtime.clr.client :as c] [portal.runtime.fs :as fs] [portal.runtime.json :as json] + [portal.runtime.shell :as shell]) + :lpy (:require [clojure.string :as str] + [portal.runtime :as rt] + [portal.runtime.python.client :as c] + [portal.runtime.fs :as fs] + [portal.runtime.json :as json] [portal.runtime.shell :as shell])) #?(:cljr (:import [System.Runtime.InteropServices OSPlatform RuntimeInformation]))) @@ -88,7 +94,7 @@ (fn [d] (try (fs/list d) - (catch #?(:cljs :default :default Exception) _))) + (catch #?(:cljs :default :default Exception) _ nil))) (fs/join root (get-windows-user) @@ -102,7 +108,7 @@ (when (str/ends-with? file (str app-name ".lnk")) {:app-id (str/replace (fs/basename (fs/dirname file)) "_crx_" "")})) (windows-chrome-web-applications)) - (catch #?(:cljs :default :default Exception) _))) + (catch #?(:cljs :default :default Exception) _ nil))) (defn- get-app-id-profile "Returns app-id and profile if portal is installed as `app-name` under any of the browser profiles" diff --git a/src/portal/runtime/cson.cljc b/src/portal/runtime/cson.cljc index d3cdfe08..347708eb 100644 --- a/src/portal/runtime/cson.cljc +++ b/src/portal/runtime/cson.cljc @@ -15,29 +15,42 @@ (:require [goog.crypt.base64 :as Base64] [portal.runtime.json-buffer :as json] - [portal.runtime.macros :as m])) + [portal.runtime.macros :as m]) + :lpy + (:require [portal.runtime.json-buffer :as json])) #?(:clj (:import [java.net URL] [java.util Base64 Date UUID]) :joyride (:import) :org.babashka/nbb (:import) - :cljs (:import [goog.math Long]))) + :cljs (:import [goog.math Long]) + :lpy (:import [basilisp.lang :as lang] + [datetime :as datetime] + [fractions :as fractions] + [math :as math] + [uuid :as uuid]))) -(defprotocol ToJson (-to-json [value buffer])) +(defprotocol ToJson (to-json* [value buffer])) (declare ->value) (defonce ^:dynamic *options* nil) +(defonce ^:private ^:dynamic *to-json* nil) (defn- transform [value] (if-let [f (:transform *options*)] (f value) value)) -(defn- to-json [buffer value] (-to-json (transform value) buffer)) +(defn- to-json [buffer value] (*to-json* buffer (transform value))) + +(defn- get-to-json [] + (let [to-json-capture *to-json*] + (fn [buffer value] + (to-json-capture buffer (transform value))))) (defn tag [buffer tag value] (assert tag string?) - (-to-json value (json/push-string buffer tag))) + (to-json (json/push-string buffer tag) value)) (defn- box-long [buffer value] #?(:cljr @@ -59,41 +72,55 @@ :cljs (-> buffer (json/push-string "long") - (json/push-string (str value))))) + (json/push-string (str value))) + :lpy + (let [js-min-int -9007199254740991 + js-max-int 900719925474099] + (if (<= js-min-int value js-max-int) + (json/push-long buffer value) + (-> buffer + (json/push-string "long") + (json/push-string (str value))))))) #?(:joyride (def Long js/Number)) #?(:org.babashka/nbb (def Long js/Number)) (extend-type #?(:cljr System.Int64 :clj Long - :cljs Long) + :cljs Long + :lpy python/int) ToJson - (-to-json [value buffer] (box-long buffer value))) + (to-json* [value buffer] (box-long buffer value))) (defn- ->long [buffer] #?(:clj (Long/parseLong (json/next-string buffer)) :cljr (System.Int64/Parse (json/next-string buffer)) - :cljs (.fromString Long (json/next-string buffer)))) + :cljs (.fromString Long (json/next-string buffer)) + :lpy (int (json/next-string buffer)))) (defn is-finite? [value] #?(:clj (Double/isFinite ^Double value) :cljr (Double/IsFinite ^Double value) - :cljs (.isFinite js/Number value))) + :cljs (.isFinite js/Number value) + :lpy (math/isfinite value))) (defn nan? [value] #?(:clj (.equals ^Double value ##NaN) :cljr (Double/IsNaN value) - :cljs (.isNaN js/Number value))) + :cljs (.isNaN js/Number value) + :lpy (math/isnan value))) (defn inf? [value] #?(:clj (.equals ^Double value ##Inf) :cljr (Double/IsInfinity value) - :cljs (== ##Inf value))) + :cljs (== ##Inf value) + :lpy (and (math/isinf value) (> value 0)))) (defn -inf? [value] #?(:clj (.equals ^Double value ##-Inf) :cljr (Double/IsNegativeInfinity value) - :cljs (== ##-Inf value))) + :cljs (== ##-Inf value) + :lpy (and (math/isinf value) (< value 0)))) (defn- push-double [buffer value] (cond @@ -110,24 +137,26 @@ (defn ->double [buffer] (double (json/next-double buffer))) -#?(:clj (extend-type Byte ToJson (-to-json [value buffer] (json/push-long buffer value)))) -#?(:clj (extend-type Short ToJson (-to-json [value buffer] (json/push-long buffer value)))) -#?(:clj (extend-type Integer ToJson (-to-json [value buffer] (json/push-long buffer value)))) -#?(:clj (extend-type Float ToJson (-to-json [value buffer] (push-double buffer value)))) -#?(:clj (extend-type Double ToJson (-to-json [value buffer] (push-double buffer value)))) +#?(:clj (extend-type Byte ToJson (to-json* [value buffer] (json/push-long buffer value)))) +#?(:clj (extend-type Short ToJson (to-json* [value buffer] (json/push-long buffer value)))) +#?(:clj (extend-type Integer ToJson (to-json* [value buffer] (json/push-long buffer value)))) +#?(:clj (extend-type Float ToJson (to-json* [value buffer] (push-double buffer value)))) +#?(:clj (extend-type Double ToJson (to-json* [value buffer] (push-double buffer value)))) + +#?(:cljr (extend System.Byte ToJson {:to-json* (fn [value buffer] (json/push-long buffer value))})) +#?(:cljr (extend System.Int16 ToJson {:to-json* (fn [value buffer] (json/push-long buffer value))})) +#?(:cljr (extend System.Int32 ToJson {:to-json* (fn [value buffer] (json/push-long buffer value))})) -#?(:cljr (extend System.Byte ToJson {:-to-json (fn [value buffer] (json/push-long buffer value))})) -#?(:cljr (extend System.Int16 ToJson {:-to-json (fn [value buffer] (json/push-long buffer value))})) -#?(:cljr (extend System.Int32 ToJson {:-to-json (fn [value buffer] (json/push-long buffer value))})) +#?(:cljr (extend-type System.Double ToJson (to-json* [value buffer] (push-double buffer value)))) -#?(:cljr (extend-type System.Double ToJson (-to-json [value buffer] (push-double buffer value)))) +#?(:cljs (extend-type number ToJson (to-json* [value buffer] (push-double buffer value)))) -#?(:cljs (extend-type number ToJson (-to-json [value buffer] (push-double buffer value)))) +#?(:lpy (extend-type python/float ToJson (to-json* [value buffer] (push-double buffer value)))) #?(:clj (extend-type clojure.lang.Ratio ToJson - (-to-json [value buffer] + (to-json* [value buffer] (-> buffer (json/push-string "R") (json/push-long (numerator value)) @@ -135,7 +164,7 @@ :cljr (extend-type clojure.lang.Ratio ToJson - (-to-json [value buffer] + (to-json* [value buffer] (-> buffer (json/push-string "R") (json/push-long (long (numerator value))) @@ -145,7 +174,7 @@ :cljs (deftype Ratio [numerator denominator] ToJson - (-to-json [_ buffer] + (to-json* [_ buffer] (-> buffer (json/push-string "R") (json/push-long numerator) @@ -154,7 +183,15 @@ (-pr-writer [_this writer _opts] (-write writer (str numerator)) (-write writer "/") - (-write writer (str denominator))))) + (-write writer (str denominator)))) + :lpy + (extend-type fractions/Fraction + ToJson + (to-json* [value buffer] + (-> buffer + (json/push-string "R") + (json/push-long (long (numerator value))) + (json/push-long (long (denominator value))))))) (defn ->ratio [buffer] (let [n (json/next-long buffer) @@ -164,29 +201,36 @@ :cljs (Ratio. n d) :default (/ n d)))) +(defn- push-string [buffer value] + (-> buffer + (json/push-string "s") + (json/push-string value))) + (extend-type #?(:cljr System.String :clj String - :cljs string) + :cljs string + :lpy python/str) ToJson - (-to-json [value buffer] - (-> buffer - (json/push-string "s") - (json/push-string value)))) + (to-json* [value buffer] (push-string buffer value))) #?(:cljr (extend System.Boolean ToJson - {:-to-json (fn [value buffer] (json/push-bool buffer value))}) + {:to-json* (fn [value buffer] (json/push-bool buffer value))}) :clj (extend-type Boolean ToJson - (-to-json [value buffer] (json/push-bool buffer value))) + (to-json* [value buffer] (json/push-bool buffer value))) :cljs (extend-type boolean ToJson - (-to-json [value buffer] (json/push-bool buffer value)))) + (to-json* [value buffer] (json/push-bool buffer value))) + :lpy + (extend-type python/bool + ToJson + (to-json* [value buffer] (json/push-bool buffer value)))) -(extend-type nil ToJson (-to-json [_value buffer] (json/push-null buffer))) +(extend-type nil ToJson (to-json* [_value buffer] (json/push-null buffer))) (defn- can-meta? [value] #?(:clj (instance? clojure.lang.IObj value) @@ -197,7 +241,8 @@ :org.babashka/nbb (try (with-meta value {}) true (catch :default _e false)) - :cljs (implements? IMeta value))) + :cljs (implements? IMeta value) + :lpy (or (coll? value) (symbol? value)))) (defn- ->meta [buffer] (let [m (->value buffer) v (->value buffer)] @@ -216,13 +261,15 @@ [tagged] #?(:bb (vary-meta tagged dissoc :type) :default tagged)) +(defn- push-tagged [buffer value] + (-> buffer + (tagged-meta (bb-fix value)) + (json/push-string (:tag value)) + (to-json (:rep value)))) + (defrecord Tagged [tag rep] ToJson - (-to-json [this buffer] - (-to-json (:rep this) - (-> buffer - (tagged-meta (bb-fix this)) - (json/push-string (:tag this)))))) + (to-json* [this buffer] (push-tagged buffer this))) (defmulti tagged-str :tag) (defmethod tagged-str :default @@ -240,11 +287,13 @@ (-pr-writer [this writer _opts] (-write writer (tagged-str this))))) -(defn tagged-value [tag rep] {:pre [(string? tag)]} (->Tagged tag rep)) +(defn tagged-value [tag rep] {:pre [(string? tag)]} + #?(:lpy (assert (string? tag) "only string tags allowed")) + (->Tagged tag rep)) (defn tagged-value? [x] (instance? Tagged x)) -(defn base64-encode ^String [byte-array] +(defn base64-encode [byte-array] #?(:clj (.encodeToString (Base64/getEncoder) byte-array) :joyride (.toString (.from js/Buffer byte-array) "base64") :org.babashka/nbb (.toString (.from js/Buffer byte-array) "base64") @@ -260,62 +309,65 @@ (extend-type #?(:clj #_:clj-kondo/ignore (Class/forName "[B") :cljr (Type/GetType "System.Byte[]") - :cljs js/Uint8Array) + :cljs js/Uint8Array + :lpy python/bytearray) ToJson - (-to-json [value buffer] + (to-json* [value buffer] (-> buffer (json/push-string "bin") (json/push-string (base64-encode value))))) (defn- ->bin [buffer] (base64-decode (json/next-string buffer))) +(defn- bigint? [value] + #?(:clj (instance? clojure.lang.BigInt value) + :cljr (instance? clojure.lang.BigInt value) + :cljs (identical? js/BigInt (some-> value .-constructor)) + :else false)) + +(defn push-bigint [buffer value] + (-> buffer + (json/push-string "N") + (json/push-string (str value)))) + #?(:clj (extend-type clojure.lang.BigInt ToJson - (-to-json [value buffer] - (-> buffer - (json/push-string "N") - (json/push-string (str value))))) + (to-json* [value buffer] (push-bigint buffer value))) :cljr (extend-type clojure.lang.BigInt ToJson - (-to-json [value buffer] - (-> buffer - (json/push-string "N") - (json/push-string (str value)))))) + (to-json* [value buffer] (push-bigint buffer value)))) #?(:cljs (m/extend-type? js/BigInt ToJson - (-to-json [value buffer] - (-> buffer - (json/push-string "N") - (json/push-string (str value)))))) + (to-json* [value buffer] (push-bigint buffer value)))) (defn- ->bigint [buffer] #?(:clj (bigint (json/next-string buffer)) :cljr (bigint (json/next-string buffer)) :cljs (js/BigInt (json/next-string buffer)))) +(defn- push-char [buffer value] (tag buffer "C" (int value))) + #?(:clj (extend-type Character ToJson - (-to-json [value buffer] - (tag buffer "C" (int value)))) + (to-json* [value buffer] (push-char buffer value))) :cljr (extend System.Char ToJson - {:-to-json (fn [value buffer] - (tag buffer "C" (int value)))}) + {:to-json* (fn [value buffer] (push-char buffer value))}) :joyride nil :org.babashka/nbb nil :cljs (deftype Character [code] ToJson - (-to-json [_this buffer] - (tag buffer "C" code)) + (to-json* [_this buffer] + (push-char buffer code)) IHash (-hash [_this] code) IEquiv @@ -335,6 +387,13 @@ 13 "return" (.fromCharCode js/String code)))))) +(defn- is-char? [value] + #?(:joyride false + :org.babashka/nbb false + :cljs (instance? Character value) + :lpy false + :default (char? value))) + (defn- ->char [buffer] #?(:clj (char (->value buffer)) :cljr (char (->value buffer)) @@ -344,51 +403,62 @@ #?(:bb (defn inst-ms [inst] (.getTime inst))) +(defn- push-inst [buffer value] + (-> buffer + (json/push-string "inst") + (json/push-long + #?(:cljr (inst-ms (.ToUniversalTime value)) + :lpy (int (* 1000 (.timestamp value))) + :default (inst-ms value))))) + (extend-type #?(:clj Date :cljr System.DateTime - :cljs js/Date) + :cljs js/Date + :lpy datetime/datetime) ToJson - (-to-json [value buffer] - (-> buffer - (json/push-string "inst") - (json/push-long - (inst-ms - #?(:cljr (.ToUniversalTime value) :default value)))))) + (to-json* [value buffer] + (push-inst buffer value))) (defn- ->inst [buffer] #?(:clj (Date. ^long (json/next-long buffer)) :cljr (.UtcDateTime (System.DateTimeOffset/FromUnixTimeMilliseconds (json/next-long buffer))) - :cljs (js/Date. (json/next-long buffer)))) + :cljs (js/Date. (json/next-long buffer)) + :lpy (datetime.datetime/utcfromtimestamp (/ (json/next-long buffer) 1000.0)))) #?(:joyride (def UUID (type (random-uuid)))) +(defn- push-uuid [buffer value] + (-> buffer + (json/push-string "uuid") + (json/push-string (str value)))) + (extend-type #?(:clj UUID :cljr System.Guid - :cljs UUID) + :cljs UUID + :lpy uuid/UUID) ToJson - (-to-json [value buffer] - (-> buffer - (json/push-string "uuid") - (json/push-string (str value))))) + (to-json* [value buffer] + (push-uuid buffer value))) (defn- ->uuid [buffer] #?(:clj (UUID/fromString (json/next-string buffer)) :cljr (System.Guid/Parse (json/next-string buffer)) - :cljs (uuid (json/next-string buffer)))) + :cljs (uuid (json/next-string buffer)) + :lpy (uuid/UUID (json/next-string buffer)))) #?(:clj (extend-type URL ToJson - (-to-json [value buffer] + (to-json* [value buffer] (-> buffer (json/push-string "url") (json/push-string (str value))))) :cljr (extend-type System.Uri ToJson - (-to-json [value buffer] + (to-json* [value buffer] (-> buffer (json/push-string "url") (json/push-string (str value)))))) @@ -397,7 +467,7 @@ (m/extend-type? js/URL ToJson - (-to-json [value buffer] + (to-json* [value buffer] (-> buffer (json/push-string "url") (json/push-string (str value)))))) @@ -409,19 +479,23 @@ #?(:joyride (def Keyword (type :kw))) +(defn- push-keyword [buffer value] + (if-let [ns (namespace value)] + (-> buffer + (json/push-string ";") + (json/push-string ns) + (json/push-string (name value))) + (-> buffer + (json/push-string ":") + (json/push-string (name value))))) + (extend-type #?(:clj clojure.lang.Keyword :cljr clojure.lang.Keyword - :cljs Keyword) + :cljs Keyword + :lpy lang.keyword/Keyword) ToJson - (-to-json [value buffer] - (if-let [ns (namespace value)] - (-> buffer - (json/push-string ";") - (json/push-string ns) - (json/push-string (name value))) - (-> buffer - (json/push-string ":") - (json/push-string (name value)))))) + (to-json* [value buffer] + (push-keyword buffer value))) (defn- ->keyword [buffer] (keyword (json/next-string buffer))) @@ -431,21 +505,25 @@ #?(:joyride (def Symbol (type 'sym))) +(defn- push-symbol [buffer value] + (if-let [ns (namespace value)] + (-> buffer + (tagged-meta value) + (json/push-string "%") + (json/push-string ns) + (json/push-string (name value))) + (-> buffer + (tagged-meta value) + (json/push-string "$") + (json/push-string (name value))))) + (extend-type #?(:clj clojure.lang.Symbol :cljr clojure.lang.Symbol - :cljs Symbol) + :cljs Symbol + :lpy lang.symbol/Symbol) ToJson - (-to-json [value buffer] - (if-let [ns (namespace value)] - (-> buffer - (tagged-meta value) - (json/push-string "%") - (json/push-string ns) - (json/push-string (name value))) - (-> buffer - (tagged-meta value) - (json/push-string "$") - (json/push-string (name value)))))) + (to-json* [value buffer] + (push-symbol buffer value))) (defn- ->symbol [buffer] (symbol (json/next-string buffer))) @@ -458,7 +536,7 @@ (tagged-coll buffer tag (meta value) value)) ([buffer tag meta-map value] (reduce - to-json + (get-to-json) (-> buffer (push-meta meta-map) (json/push-string tag) @@ -479,7 +557,7 @@ #?(:clj (extend-type clojure.lang.StringSeq ToJson - (-to-json [value buffer] + (to-json* [value buffer] (tagged-coll buffer "(" value)))) (def coll-types @@ -518,6 +596,7 @@ clojure.lang.APersistentMap+KeySeq clojure.lang.APersistentMap+ValSeq clojure.lang.LongRange + clojure.lang.Range clojure.lang.Repeat clojure.lang.PersistentList clojure.lang.PersistentQueue @@ -555,6 +634,7 @@ cljs.core/KeySeq cljs.core/ValSeq cljs.core/Repeat + cljs.core/Range cljs.core/List cljs.core/ChunkedCons cljs.core/ChunkedSeq @@ -564,21 +644,28 @@ cljs.core/PersistentArrayMapSeq cljs.core/PersistentTreeMapSeq cljs.core/NodeSeq - cljs.core/ArrayNodeSeq])) + cljs.core/ArrayNodeSeq] + :lpy + [lang.seq/LazySeq + lang.list/PersistentList])) (doseq [coll-type coll-types] #?(:clj (extend coll-type ToJson - {:-to-json (fn [value buffer] (tagged-coll buffer "(" value))}) + {:to-json* (fn [value buffer] (tagged-coll buffer "(" value))}) :cljr (extend coll-type ToJson - {:-to-json (fn [value buffer] (tagged-coll buffer "(" value))}) + {:to-json* (fn [value buffer] (tagged-coll buffer "(" value))}) :cljs (extend-type coll-type ToJson - (-to-json [value buffer] (tagged-coll buffer "(" value))))) + (to-json* [value buffer] (tagged-coll buffer "(" value))) + :lpy + (extend-type coll-type + ToJson + (to-json* [value buffer] (tagged-coll buffer "(" value))))) #?(:org.babashka/nbb nil :cljs @@ -586,16 +673,12 @@ ^:cljs.analyzer/no-resolve cljs.core/IntegerRange ToJson - (-to-json [value buffer] (tagged-coll buffer "(" value)))) - -#?(:joyride (def Range (type (range)))) -#?(:org.babashka/nbb (def Range (type (range)))) + (to-json* [value buffer] (tagged-coll buffer "(" value)))) -(extend-type #?(:clj clojure.lang.Range - :cljr clojure.lang.Range - :cljs Range) - ToJson - (-to-json [value buffer] (tagged-coll buffer "(" (into [] value)))) +#?(:clj + (extend-type clojure.lang.Range + ToJson + (to-json* [value buffer] (tagged-coll buffer "(" (into [] value))))) (def vector-types #?(:clj @@ -617,21 +700,26 @@ :cljs [cljs.core/PersistentVector cljs.core/Subvec - cljs.core/MapEntry])) + cljs.core/MapEntry] + :lpy [lang.vector/PersistentVector])) (doseq [vector-type vector-types] #?(:clj (extend vector-type ToJson - {:-to-json (fn [value buffer] (tagged-coll buffer "[" value))}) + {:to-json* (fn [value buffer] (tagged-coll buffer "[" value))}) :cljr (extend vector-type ToJson - {:-to-json (fn [value buffer] (tagged-coll buffer "[" value))}) + {:to-json* (fn [value buffer] (tagged-coll buffer "[" value))}) :cljs (extend-protocol ToJson vector-type - (-to-json [value buffer] (tagged-coll buffer "[" value))))) + (to-json* [value buffer] (tagged-coll buffer "[" value))) + :lpy + (extend-protocol ToJson + vector-type + (to-json* [value buffer] (tagged-coll buffer "[" value))))) (defn- ->into [zero buffer] (let [n (json/next-long buffer)] @@ -647,28 +735,39 @@ (extend-type #?(:clj clojure.lang.PersistentHashSet :cljr clojure.lang.PersistentHashSet - :cljs PersistentHashSet) + :cljs PersistentHashSet + :lpy lang.set/PersistentSet) ToJson - (-to-json [value buffer] (tagged-coll buffer "#" value))) + (to-json* [value buffer] (tagged-coll buffer "#" value))) #?(:joyride (def PersistentTreeSet (type (sorted-set)))) #?(:org.babashka/nbb (def PersistentTreeSet (type (sorted-set)))) -(extend-type #?(:clj clojure.lang.PersistentTreeSet - :cljr clojure.lang.PersistentTreeSet - :cljs PersistentTreeSet) - ToJson - (-to-json [value buffer] (tagged-coll buffer "sset" value))) +#?(:clj + (extend-type clojure.lang.PersistentTreeSet + ToJson + (to-json* [value buffer] (tagged-coll buffer "sset" value))) + :cljr + (extend-type clojure.lang.PersistentTreeSet + ToJson + (to-json* [value buffer] (tagged-coll buffer "sset" value))) + :cljs + (extend-type PersistentTreeSet + ToJson + (to-json* [value buffer] (tagged-coll buffer "sset" value)))) (defn- ->sset [buffer] - (let [n (json/next-long buffer) - values (for [_ (range n)] (->value buffer)) - order (zipmap values (range))] - (into - (sorted-set-by - (fn [a b] - (compare (get order a) (get order b)))) - values))) + #?(:lpy + (->into #{} buffer) + :default + (let [n (json/next-long buffer) + values (for [_ (range n)] (->value buffer)) + order (zipmap values (range))] + (into + (sorted-set-by + (fn [a b] + (compare (get order a) (get order b)))) + values)))) (defn tagged-map ([buffer value] @@ -676,54 +775,81 @@ ([buffer tag value] (tagged-map buffer tag (meta value) value)) ([buffer tag meta-map value] - (reduce-kv - (fn [buffer k v] + (let [f (get-to-json)] + (reduce-kv + (fn [buffer k v] + (-> buffer + (f k) + (f v))) (-> buffer - (to-json k) - (to-json v))) - (-> buffer - (push-meta meta-map) - (json/push-string tag) - (json/push-long (count value))) - value))) + (push-meta meta-map) + (json/push-string tag) + (json/push-long (count value))) + value)))) #?(:joyride (def PersistentHashMap (type (hash-map)))) #?(:org.babashka/nbb (def PersistentHashMap (type (hash-map)))) (extend-type #?(:clj clojure.lang.PersistentHashMap :cljr clojure.lang.PersistentHashMap - :cljs PersistentHashMap) + :cljs PersistentHashMap + :lpy lang.map/PersistentMap) ToJson - (-to-json [value buffer] (tagged-map buffer value))) + (to-json* [value buffer] (tagged-map buffer value))) #?(:joyride (def PersistentTreeMap (type (sorted-map)))) #?(:org.babashka/nbb (def PersistentTreeMap (type (sorted-map)))) -(extend-type #?(:clj clojure.lang.PersistentTreeMap - :cljr clojure.lang.PersistentTreeMap - :cljs PersistentTreeMap) - ToJson - (-to-json [value buffer] (tagged-map buffer "smap" value))) +#?(:clj + (extend-type clojure.lang.PersistentTreeMap + ToJson + (to-json* [value buffer] (tagged-map buffer "smap" value))) + :cljr + (extend-type clojure.lang.PersistentTreeMap + ToJson + (to-json* [value buffer] (tagged-map buffer "smap" value))) + :cljs + (extend-type PersistentTreeMap + ToJson + (to-json* [value buffer] (tagged-map buffer "smap" value)))) #?(:clj (extend-type clojure.lang.APersistentMap ToJson - (-to-json [value buffer] (tagged-map buffer value)))) + (to-json* [value buffer] (tagged-map buffer value)))) #?(:joyride (def PersistentArrayMap (type {}))) #?(:org.babashka/nbb (def PersistentArrayMap (type {}))) -(extend-type #?(:clj clojure.lang.PersistentArrayMap - :cljr clojure.lang.PersistentArrayMap - :cljs PersistentArrayMap) - ToJson - (-to-json [value buffer] (tagged-map buffer value))) +#?(:clj + (extend-type clojure.lang.PersistentArrayMap + ToJson + (to-json* [value buffer] (tagged-map buffer value))) + :cljr + (extend-type clojure.lang.PersistentArrayMap + ToJson + (to-json* [value buffer] (tagged-map buffer value))) + :cljs + (extend-type PersistentArrayMap + ToJson + (to-json* [value buffer] (tagged-map buffer value)))) -(extend-type #?(:clj clojure.lang.IRecord - :cljr clojure.lang.IRecord - :cljs cljs.core/IRecord) - ToJson - (-to-json [value buffer] (tagged-map buffer value))) +#?(:clj + (extend-type clojure.lang.IRecord + ToJson + (to-json* [value buffer] (tagged-map buffer value))) + :cljr + (extend-type clojure.lang.IRecord + ToJson + (to-json* [value buffer] (tagged-map buffer value))) + :cljs + (extend-type cljs.core/IRecord + ToJson + (to-json* [value buffer] (tagged-map buffer value))) + :lpy + (extend-type lang.interfaces/IRecord + ToJson + (to-json* [value buffer] (tagged-map buffer value)))) (defn- ->map [buffer] (let [n (json/next-long buffer)] @@ -735,37 +861,61 @@ (assoc! m (->value buffer) (->value buffer))))))) (defn- ->sorted-map [buffer] - (let [n (json/next-long buffer) - pairs (for [_ (range n)] - [(->value buffer) (->value buffer)]) - order (zipmap (map first pairs) (range))] - (into - (sorted-map-by - (fn [a b] - (compare (get order a) (get order b)))) - pairs))) + #?(:lpy + (->map buffer) + :default + (let [n (json/next-long buffer) + pairs (for [_ (range n)] + [(->value buffer) (->value buffer)]) + order (zipmap (map first pairs) (range))] + (into + (sorted-map-by + (fn [a b] + (compare (get order a) (get order b)))) + pairs)))) #?(:bb (def clojure.lang.TaggedLiteral (type (tagged-literal 'a :a)))) #?(:joyride (def TaggedLiteral (type (tagged-literal 'f :v)))) #?(:org.babashka/nbb (def TaggedLiteral (type (tagged-literal 'f :v)))) -(extend-type #?(:clj clojure.lang.TaggedLiteral - :cljr clojure.lang.TaggedLiteral - :cljs TaggedLiteral) - ToJson - (-to-json [{:keys [tag form]} buffer] - (-> buffer - (json/push-string "tag") - (json/push-string - (if-let [ns (namespace tag)] - (str ns "/" (name tag)) - (name tag))) - (to-json form)))) +(defn- push-tagged-literal [buffer {:keys [tag form]}] + (-> buffer + (json/push-string "tag") + (json/push-string + (if-let [ns (namespace tag)] + (str ns "/" (name tag)) + (name tag))) + (to-json form))) + +#?(:clj + (extend-type clojure.lang.TaggedLiteral + ToJson + (to-json* [value buffer] + (push-tagged-literal buffer value))) + :cljr + (extend-type clojure.lang.TaggedLiteral + ToJson + (to-json* [value buffer] + (push-tagged-literal buffer value))) + :cljs + (extend-type TaggedLiteral + ToJson + (to-json* [value buffer] + (push-tagged-literal buffer value))) + :lpy + (extend-type lang.tagged/TaggedLiteral + ToJson + (to-json* [value buffer] + (push-tagged-literal buffer value)))) (defn- ->tagged-literal [buffer] (tagged-literal (symbol (json/next-string buffer)) (->value buffer))) +(defn- ->list [buffer] + #?(:lpy (or (seq (->into [] buffer)) '()) + :default (or (list* (->into [] buffer)) '()))) + #?(:clj (defn- eq ^Boolean [^String a b] (.equals a b))) (defn- ->value [buffer] @@ -773,14 +923,14 @@ (if-not (string? op) op (transform - (#?@(:bb [case] :cljr [case] :clj [condp eq] :cljs [case]) + (#?@(:bb [case] :cljr [case] :clj [condp eq] :cljs [case] :lpy [condp identical?]) op "s" (json/next-string buffer) ":" (->keyword buffer) "{" (->map buffer) "$" (->symbol buffer) "[" (->into [] buffer) - "(" (or (list* (->into [] buffer)) '()) + "(" (->list buffer) ";" (->keyword-2 buffer) "%" (->symbol-2 buffer) "#" (->into #{} buffer) @@ -803,14 +953,122 @@ (let [handler (:default-handler *options* tagged-value)] (handler op (->value buffer)))))))) +(defn- to-json-proto [buffer value] (to-json* value buffer)) + +(defn- range? [value] + #?(:clj (instance? clojure.lang.Range value) + :cljr (instance? clojure.lang.Range value) + :org.babashka/nbb false + :cljs (instance? Range value))) + +#?(:lpy (def sorted? (constantly false))) + +(defn- to-json-cond [buffer value] + (if (coll? value) + (cond + (tagged-value? value) + (push-tagged buffer value) + + (map? value) #?(:lpy (tagged-map buffer value) + :default + (cond + (sorted? value) (tagged-map buffer "smap" value) + :else (tagged-map buffer value))) + + (vector? value) (tagged-coll buffer "[" value) + (set? value) #?(:lpy (tagged-coll buffer "#" value) + :default + (cond + (sorted? value) (tagged-coll buffer "sset" value) + :else (tagged-coll buffer "#" value))) + (coll? value) #?(:lpy (tagged-coll buffer "(" value) + :default + (cond + (range? value) (tagged-coll buffer "(" (into [] value)) + :else (tagged-coll buffer "(" value))) + + (nil? (::dispatch *options*)) + (binding [*to-json* to-json-proto] + (to-json buffer value)) + + :else (throw (ex-info "Unknown value type" {:value value}))) + (cond + (number? value) (cond + (float? value) (push-double buffer value) + :else (box-long buffer value)) + + (string? value) (push-string buffer value) + (boolean? value) (json/push-bool buffer value) + (nil? value) (json/push-null buffer) + + (keyword? value) (push-keyword buffer value) + (symbol? value) (push-symbol buffer value) + + (bigint? value) (push-bigint buffer value) + (uuid? value) (push-uuid buffer value) + (inst? value) (push-inst buffer value) + + (tagged-literal? value) + (push-tagged-literal buffer value) + + (is-char? value) (push-char buffer value) + + (nil? (::dispatch *options*)) + (binding [*to-json* to-json-proto] + (to-json buffer value)) + + :else (throw (ex-info "Unknown value type" {:value value})))) + #_(cond + (tagged-value? value) + (push-tagged buffer value) + + (nil? value) (json/push-null buffer) + (boolean? value) (json/push-bool buffer value) + (is-char? value) (push-char buffer value) + (string? value) (push-string buffer value) + (bigint? value) (push-bigint buffer value) + + (number? value) (cond + (float? value) (push-double buffer value) + :else (box-long buffer value)) + + (keyword? value) (push-keyword buffer value) + (symbol? value) (push-symbol buffer value) + + (map? value) (cond + (sorted? value) (tagged-map buffer "smap" value) + :else (tagged-map buffer value)) + + (vector? value) (tagged-coll buffer "[" value) + (set? value) (cond + (sorted? value) (tagged-coll buffer "sset" value) + :else (tagged-coll buffer "#" value)) + (coll? value) (cond + (range? value) (tagged-coll buffer "(" (into [] value)) + :else (tagged-coll buffer "(" value)) + (uuid? value) (push-uuid buffer value) + (inst? value) (push-inst buffer value) + + (tagged-literal? value) + (push-tagged-literal buffer value) + + (nil? (::dispatch *options*)) + (binding [*to-json* to-json-proto] + (to-json buffer value)) + + :else (throw (ex-info "Unknown value type" {:value value})))) + (defn write ([value] (write value nil)) ([value options] - (binding [*options* options] + (binding [*options* options + *to-json* (case (::dispatch options :prototype) + :prototype to-json-proto + :cond to-json-cond)] (json/with-buffer to-json value)))) (defn read ([string] (read string nil)) ([string options] (binding [*options* options] - (->value (json/->reader string))))) + (->value (json/->reader string))))) \ No newline at end of file diff --git a/src/portal/runtime/fs.cljc b/src/portal/runtime/fs.cljc index 7f581425..863b6ae8 100644 --- a/src/portal/runtime/fs.cljc +++ b/src/portal/runtime/fs.cljc @@ -6,33 +6,42 @@ ["os" :as os] ["path" :as path] [clojure.string :as s]) - :cljr (:require [clojure.string :as s])) - #?(:cljr (:import (System.IO Directory File Path)))) + :cljr (:require [clojure.string :as s]) + :lpy (:require [clojure.string :as s])) + #?(:cljr (:import (System.IO Directory File Path)) + :lpy (:import [pathlib :as p] + [os :as os] + [shutil :as shutil]))) (defn cwd [] #?(:clj (System/getProperty "user.dir") :cljs (.cwd js/process) - :cljr (Directory/GetCurrentDirectory))) + :cljr (Directory/GetCurrentDirectory) + :lpy (os/getcwd))) (defn slurp [path] #?(:clj (clojure.core/slurp path) :cljs (fs/readFileSync path "utf8") - :cljr (clojure.core/slurp path :enc "utf8"))) + :cljr (clojure.core/slurp path :enc "utf8") + :lpy (basilisp.core/slurp path))) (defn spit [path content] #?(:clj (clojure.core/spit path content) :cljs (fs/writeFileSync path content) - :cljr (clojure.core/spit path content))) + :cljr (clojure.core/spit path content) + :lpy (basilisp.core/spit path content))) (defn mkdir [path] #?(:clj (.mkdirs (io/file path)) - :cljs (fs/mkdirSync path #js {:recursive true}) - :cljr (Directory/CreateDirectory path))) + :cljs (fs/mkdirSync path (clj->js {:recursive true})) + :cljr (Directory/CreateDirectory path) + :lpy (os/makedirs path))) (defn path [] #?(:clj (System/getenv "PATH") :cljs (.-PATH js/process.env) - :cljr (Environment/GetEnvironmentVariable "PATH"))) + :cljr (Environment/GetEnvironmentVariable "PATH") + :lpy (.get os/environ "PATH"))) (defn separator [] (or #?(:clj (System/getProperty "path.separator") @@ -43,14 +52,16 @@ (defn join [& paths] #?(:clj (.getCanonicalPath ^java.io.File (apply io/file paths)) :cljs (apply path/join paths) - :cljr (Path/Join (into-array String paths)))) + :cljr (Path/Join (into-array String paths)) + :lpy (apply os.path/join paths))) (defn exists [f] (when (and (string? f) #?(:clj (.exists (io/file f)) :cljs (fs/existsSync f) :cljr (or (File/Exists f) - (Directory/Exists f)))) + (Directory/Exists f)) + :lpy (.is_file (p/Path f)))) f)) (defn is-file [f] @@ -58,7 +69,8 @@ #?(:clj (.isFile (io/file f)) :cljs (and (exists f) (.isFile (fs/lstatSync f))) - :cljr (File/Exists f))) + :cljr (File/Exists f) + :lpy (.is_file (p/Path f)))) f)) (defn modified [f] @@ -66,7 +78,8 @@ #?(:clj (.lastModified (io/file f)) :cljs (.getTime ^js/Date (.-mtime (fs/lstatSync f))) :cljr (.ToUnixTimeMilliseconds - (DateTimeOffset. (File/GetLastWriteTime f)))))) + (DateTimeOffset. (File/GetLastWriteTime f))) + :lpy (os.path/getmtime f)))) (defn can-execute [f] #?(:clj (let [file (io/file f)] @@ -75,7 +88,8 @@ (try (fs/accessSync f fs/constants.X_OK) (catch :default _e true))) f) - :cljr (exists f))) + :cljr (exists f) + :lpy (os/access f os/X_OK))) (defn paths [] (s/split (or (path) "") (re-pattern (separator)))) @@ -91,21 +105,25 @@ (defn home [] #?(:clj (System/getProperty "user.home") :cljs (os/homedir) - :cljr (Environment/GetFolderPath System.Environment+SpecialFolder/UserProfile))) + :cljr (Environment/GetFolderPath System.Environment+SpecialFolder/UserProfile) + :lpy (p.Path/home))) (defn list [path] #?(:clj (for [^java.io.File f (.listFiles (io/file path))] (.getAbsolutePath f)) :cljs (for [f (fs/readdirSync path)] (join path f)) - :cljr (Directory/GetFileSystemEntries path))) + :cljr (Directory/GetFileSystemEntries path) + :lpy (for [child (os/listdir path)] + (join path child)))) (defn rm [path] #?(:clj (let [children (list path)] (doseq [child children] (rm child)) (io/delete-file path)) - :cljs (fs/rmSync path #js {:recursive true}) - :cljr (Directory/Delete path true))) + :cljs (fs/rmSync path (clj->js {:recursive true})) + :cljr (Directory/Delete path true) + :lpy (shutil/rmtree path))) (defn rm-exit [path] #?(:clj (.deleteOnExit (io/file path)) @@ -118,12 +136,15 @@ #?(:clj (.getParent (io/file path)) :cljs (let [root (.-root (path/parse path))] (when-not (= path root) (path/dirname path))) - :cljr (some-> (Directory/GetParent path) str))) + :cljr (some-> (Directory/GetParent path) str) + :lpy (when-not (= "/" path) + (os.path/dirname path)))) (defn basename [path] #?(:clj (.getName (io/file path)) :cljs (path/basename path) - :cljr (Path/GetFileName path))) + :cljr (Path/GetFileName path) + :lpy (os.path/basename path))) (defn file-seq [dir] (tree-seq diff --git a/src/portal/runtime/index.cljc b/src/portal/runtime/index.cljc index c9986070..ec369994 100644 --- a/src/portal/runtime/index.cljc +++ b/src/portal/runtime/index.cljc @@ -4,7 +4,7 @@ :or {name "portal" version "0.59.2" code-url "main.js" - platform #?(:bb "bb" :clj "jvm" :cljs "node" :cljr "clr")}}] + platform #?(:bb "bb" :clj "jvm" :cljs "node" :cljr "clr" :lpy "py")}}] (str "" "" diff --git a/src/portal/runtime/json.cljc b/src/portal/runtime/json.cljc index 77b660a5..9481ca65 100644 --- a/src/portal/runtime/json.cljc +++ b/src/portal/runtime/json.cljc @@ -3,13 +3,15 @@ #?(:bb (:require [cheshire.core :as json]) :clj (:require [clojure.data.json :as json]) :cljr (:require [portal.runtime.clr.assembly] - [clojure.data.json :as json]))) + [clojure.data.json :as json]) + :lpy (:require [basilisp.json :as json]))) (defn write [value] #?(:bb (json/generate-string value) :clj (json/write-str value) :cljr (json/write-str value) - :cljs (.stringify js/JSON (clj->js value)))) + :cljs (.stringify js/JSON (clj->js value)) + :lpy (json/write-str value))) (defn read ([string] @@ -20,9 +22,10 @@ :cljr (json/read-str string :key-fn (:key-fn opts)) :cljs (js->clj (.parse js/JSON string) :keywordize-keys - (= keyword (:key-fn opts)))))) + (= keyword (:key-fn opts))) + :lpy (json/read-str string :key-fn (:key-fn opts))))) (defn read-stream [stream] #?(:bb (json/parse-stream stream keyword) :clj (json/read stream :key-fn keyword) - :cljs (throw (ex-info "Unsupported in cljs" {:stream stream})))) + :default (throw (ex-info "Unsupported" {:stream stream})))) diff --git a/src/portal/runtime/json_buffer.cljc b/src/portal/runtime/json_buffer.cljc index d5d5401e..4541e70f 100644 --- a/src/portal/runtime/json_buffer.cljc +++ b/src/portal/runtime/json_buffer.cljc @@ -12,12 +12,13 @@ JsonNode JsonArray JsonValue - JsonNodeOptions)))) + JsonNodeOptions)) + :lpy (:import [json :as json]))) (defn -shift [this] (this)) #?(:cljs (defn shifter [source] - (let [this #js {:n 0}] + (let [this (clj->js {:n 0})] (fn [] (let [n (.-n this) result (aget source n)] @@ -37,37 +38,44 @@ (doto (JsonReader. (StringReader. data)) (.beginArray)) :cljs - (shifter (.parse js/JSON data)))) + (shifter (.parse js/JSON data)) + :lpy + (iter (json/loads data)))) (defn push-null [buffer] #?(:bb (conj! buffer nil) :clj (doto ^JsonWriter buffer (.nullValue)) :cljr (doto ^JsonArray buffer (.Add nil)) - :cljs (doto ^js buffer (.push nil)))) + :cljs (doto ^js buffer (.push nil)) + :lpy (doto buffer (.append nil)))) (defn push-bool [buffer value] #?(:bb (conj! buffer value) :clj (doto ^JsonWriter buffer (.value ^Boolean value)) :cljr (doto ^JsonArray buffer (.Add value)) - :cljs (doto ^js buffer (.push value)))) + :cljs (doto ^js buffer (.push value)) + :lpy (doto buffer (.append value)))) (defn push-long [buffer value] #?(:bb (conj! buffer value) :clj (doto ^JsonWriter buffer (.value ^Long value)) :cljr (doto ^JsonArray buffer (.Add value)) - :cljs (doto ^js buffer (.push value)))) + :cljs (doto ^js buffer (.push value)) + :lpy (doto buffer (.append value)))) (defn push-double [buffer value] #?(:bb (conj! buffer value) :clj (doto ^JsonWriter buffer (.value ^Double value)) :cljr (doto ^JsonArray buffer (.Add value)) - :cljs (doto ^js buffer (.push value)))) + :cljs (doto ^js buffer (.push value)) + :lpy (doto buffer (.append value)))) (defn push-string [buffer value] #?(:bb (conj! buffer value) :clj (doto ^JsonWriter buffer (.value ^String value)) :cljr (doto ^JsonArray buffer (.Add value)) - :cljs (doto ^js buffer (.push value)))) + :cljs (doto ^js buffer (.push value)) + :lpy (doto buffer (.append value)))) (defn push-value [buffer value] (cond @@ -81,7 +89,8 @@ #?(:bb (let [v (first @buffer)] (vswap! buffer rest) v) :clj (.nextNull ^JsonReader buffer) :cljr (do (vswap! buffer rest) nil) - :cljs (-shift buffer))) + :cljs (-shift buffer) + :lpy (python/next buffer))) (defn next-bool [buffer] #?(:bb (let [v (first @buffer)] (vswap! buffer rest) v) @@ -89,7 +98,8 @@ (vswap! buffer rest) (.GetValue v (type-args System.Boolean))) :clj (.nextBoolean ^JsonReader buffer) - :cljs (-shift buffer))) + :cljs (-shift buffer) + :lpy (python/next buffer))) (defn next-long ^long [buffer] #?(:bb (let [v (first @buffer)] (vswap! buffer rest) v) @@ -97,7 +107,8 @@ (vswap! buffer rest) (.GetValue v (type-args System.Int64))) :clj (.nextLong ^JsonReader buffer) - :cljs (-shift buffer))) + :cljs (-shift buffer) + :lpy (python/next buffer))) (defn next-double ^double [buffer] #?(:bb (let [v (first @buffer)] (vswap! buffer rest) v) @@ -105,15 +116,17 @@ (vswap! buffer rest) (.GetValue v (type-args System.Double))) :clj (.nextDouble ^JsonReader buffer) - :cljs (-shift buffer))) + :cljs (-shift buffer) + :lpy (python/next buffer))) -(defn next-string ^String [buffer] +(defn next-string [buffer] #?(:bb (let [v (first @buffer)] (vswap! buffer rest) v) :cljr (let [v ^JsonValue (first @buffer)] (vswap! buffer rest) (.GetValue v (type-args System.String))) :clj (.nextString ^JsonReader buffer) - :cljs (-shift buffer))) + :cljs (-shift buffer) + :lpy (python/next buffer))) (defn next-value [buffer] #?(:bb (let [v (first @buffer)] (vswap! buffer rest) v) @@ -142,7 +155,8 @@ (if (== (.indexOf n 46) -1) (Long/parseLong n) (Double/parseDouble n)))) - :cljs (-shift buffer))) + :cljs (-shift buffer) + :lpy (python/next buffer))) (defn with-buffer [f value] #?(:bb (json/write (persistent! (f (transient []) value))) @@ -156,4 +170,5 @@ (f json value) (.endArray json) (.close json) - (.toString out)))) + (.toString out)) + :lpy (json/dumps (f (python/list) value)))) diff --git a/src/portal/runtime/protocols.cljc b/src/portal/runtime/protocols.cljc new file mode 100644 index 00000000..79c803ea --- /dev/null +++ b/src/portal/runtime/protocols.cljc @@ -0,0 +1,13 @@ +(ns ^:no-doc portal.runtime.protocols + (:refer-clojure :exclude [send])) + +(defprotocol Listener + (on-open [listener socket]) + (on-message [listener socket message]) + (on-pong [listener socket data]) + (on-error [listener socket throwable]) + (on-close [listener socket code reason])) + +(defprotocol Socket + (send [socket message]) + (close [socket code reason])) \ No newline at end of file diff --git a/src/portal/runtime/python/client.lpy b/src/portal/runtime/python/client.lpy new file mode 100644 index 00000000..8e09b7a9 --- /dev/null +++ b/src/portal/runtime/python/client.lpy @@ -0,0 +1,99 @@ +(ns ^:no-doc portal.runtime.python.client + (:require [portal.runtime :as rt])) + +(def ops + {:portal.rpc/response + (fn [message _send!] + (let [id (:portal.rpc/id message)] + (when-let [response (get @rt/pending-requests id)] + (deliver response message))))}) + +(def timeout 60000) + +(defn- get-connection [session-id] + (let [p (promise) + watch-key (keyword (gensym))] + (if-let [send! (get @rt/connections session-id)] + (deliver p send!) + (add-watch + rt/connections + watch-key + (fn [_ _ _old new] + (when-let [send! (get new session-id)] + (deliver p send!))))) + (let [result (deref p timeout nil)] + (remove-watch rt/connections watch-key) + result))) + +(defn- request! [session-id message] + (if-let [send! (get-connection session-id)] + (let [id (rt/next-id) + response (promise) + message (assoc message :portal.rpc/id id)] + (swap! rt/pending-requests assoc id response) + (send! message) + (let [response (deref response timeout ::timeout)] + (swap! rt/pending-requests dissoc id) + (if-not (= response ::timeout) + response + (throw (ex-info + "Portal request timeout" + {::timeout true + :session-id session-id + :message message}))))) + (throw (ex-info "No such portal session" + {:session-id session-id :message message})))) + +(defn- broadcast! [message] + (when-let [sessions (keys @rt/connections)] + (let [response (promise)] + (doseq [session-id sessions] + (future + (try + (deliver response (request! session-id message)) + (catch Exception ex + (when (-> ex ex-data ::timeout) + (swap! rt/connections dissoc session-id)) + (deliver response ex))))) + (let [response (deref response timeout ::timeout)] + (cond + (instance? Exception response) + (throw response) + (not= response ::timeout) + response + :else + (throw (ex-info + "Portal request timeout" + {::timeout true + :session-id :all + :message message}))))))) + +(defn request + ([message] + (broadcast! message)) + ([session-id message] + (request! session-id message))) + +(defn- push-state [session-id new-value] + (request session-id {:op :portal.rpc/push-state :state new-value}) + (rt/update-selected session-id [new-value]) + new-value) + +(defrecord Portal [session-id] +;; i/IDeref +;; (deref [_this] (first (get-in @rt/sessions [session-id :selected]))) + +;; IAtom +;; (reset [_this new-value] (push-state session-id new-value)) + +;; (swap [this f] (reset! this (f @this))) +;; (swap [this f a] (reset! this (f @this a))) +;; (swap [this f a b] (reset! this (f @this a b))) +;; (swap [this f a b args] (reset! this (apply f @this a b args))) +;; (compareAndSet [_this _oldv _newv]) + ) + +(defn make-atom [session-id] (Portal. session-id)) + +(defn open? [session-id] + (contains? @rt/connections session-id)) diff --git a/src/portal/runtime/python/launcher.lpy b/src/portal/runtime/python/launcher.lpy new file mode 100644 index 00000000..590b386f --- /dev/null +++ b/src/portal/runtime/python/launcher.lpy @@ -0,0 +1,151 @@ +(ns ^:no-doc portal.runtime.python.launcher + (:require [portal.runtime :as rt] + [portal.runtime.browser :as browser] + [portal.runtime.python.client :as c] + [portal.runtime.python.server :as server] + [portal.runtime.protocols :as p]) + (:import [aiohttp.web :as web] + [asyncio :as asyncio])) + +(defn ws-send [ws message] + (try + (cond + (string? message) + (asyncio/run_coroutine_threadsafe (.send_str ws message) @rt/async-loop)) + (catch Exception ex + (tap> [:ws-send (pr-str ex)])))) + +(defn ^:async ->ws-response [request listener] + (let [ws (web/WebSocketResponse) + socket (reify p/Socket + (send [_ message] + (ws-send ws message)) + (close [_ code reason] + (.close ws)))] + (await (.prepare ws request)) + (p/on-open listener socket) + (loop [] + (let [msg (await (.receive ws))] + #_(tap> [:ws-receive (.-data msg)]) + (cond + (= (.-type msg) (.-TEXT web/WSMsgType)) + (p/on-message listener socket (.-data msg)) + + (= (.-type msg) (.-CLOSE web/WSMsgType)) + (p/on-close listener socket (.-data msg) (.-extra msg))) + (when-not (= (.-type msg) (.-CLOSE web/WSMsgType)) + (recur)))) + ws)) + +(defn- ^:async ->response [request {:keys [status body headers] :as response}] + (if-let [listener (:ring.websocket/listener response)] + (await (->ws-response request listener)) + (web/Response + ** + :status status + :body body + :headers headers))) + +(defn ^:async handler [request] + (try + (await (->response request (await (server/handler request)))) + (catch Exception ex + (tap> [:handler (pr-str ex)]) + (await (->response request {:status 500}))))) + +(defonce ^:private server (atom nil)) + +(comment + (start {:port 8080}) + (stop) + (-> @server) + (reset! server nil)) + +(defn- ^:async start-handler [options] + (try + (let [runner (web/ServerRunner (web/Server #(handler %)))] + (await (.setup runner)) + (let [site (web/TCPSite runner (:host options "localhost") (:port options 0)) + _ (await (.start site)) + event (asyncio/Event) + [host port] (.getsockname (aget (.. site -_server -sockets) 0))] + (try + (swap! server merge + {:port port + :host host + ::stop (fn [] + (let [p (promise)] + (asyncio/run_coroutine_threadsafe + (^:async + (fn [] + (.set event) + (.clear event) + (deliver p ::done))) + @rt/async-loop) + @p))}) + (deliver (:done @server) ::started) + (await (.wait event)) + (finally + (await (.cleanup runner)) + (reset! server nil))))) + (catch Exception ex + (deliver (:done @server) ex)))) + +(defn start [options] + (when-not @server + (swap! server assoc :done (promise)) + (future + (try + (reset! rt/async-loop (asyncio/new_event_loop)) + (.run_until_complete @rt/async-loop (start-handler options)) + (finally + (.close @rt/async-loop))))) + (let [result (deref (:done @server) 5000 ::timeout)] + (cond + (= ::started result) + (select-keys @server [:host :port]) + (= ::timeout result) + (throw (ex-info "Failed to start server." options)) + :else + (throw result)))) + +(defn stop [] + (when-let [{::keys [stop]} @server] + (stop))) + +(defn open + ([options] + (open nil options)) + ([portal options] + (let [server (start options)] + (browser/open {:portal portal :options options :server server})))) + +(defn clear [portal] + (if (= portal :all) + (c/request {:op :portal.rpc/clear}) + (c/request (:session-id portal) {:op :portal.rpc/clear})) + (rt/cleanup-sessions)) + +(defn close [portal] + (if (= portal :all) + (c/request {:op :portal.rpc/close}) + (c/request (:session-id portal) {:op :portal.rpc/close})) + (rt/close-session (:session-id portal)) + (rt/cleanup-sessions)) + +(defn eval-str [portal msg] + (let [response (if (= portal :all) + (c/request (assoc msg :op :portal.rpc/eval-str)) + (c/request (:session-id portal) + (assoc msg :op :portal.rpc/eval-str)))] + (if-not (:error response) + response + (throw (ex-info (:message response) response))))) + +(defn sessions [] + (for [session-id (rt/active-sessions)] (c/make-atom session-id))) + +(defn url [portal] + (browser/url {:portal portal :server @server})) + +(reset! rt/request c/request) diff --git a/src/portal/runtime/python/server.lpy b/src/portal/runtime/python/server.lpy new file mode 100644 index 00000000..ab798ae4 --- /dev/null +++ b/src/portal/runtime/python/server.lpy @@ -0,0 +1,91 @@ +(ns ^:no-doc portal.runtime.python.server + (:require [clojure.edn :as edn] + [clojure.string :as str] + [portal.runtime :as rt] + [portal.runtime.cson :as cson] + [portal.runtime.fs :as fs] + [portal.runtime.index :as index] + [portal.runtime.json :as json] + [portal.runtime.npm :as npm] + [portal.runtime.rpc :as rpc]) + (:import [uuid :as uuid])) + +(defmulti route (juxt :request-method :uri)) + +(defmethod route :default [_request] + {:status 400}) + +(defmethod route [:get "/rpc"] [request] + (let [session (rt/open-session (:session request))] + {:ring.websocket/listener (rpc/listener session)})) + +(defn- send-resource [content-type resource] + {:status 200 + :headers {"Content-Type" content-type} + :body resource}) + +(defmethod route [:get "/icon.svg"] [_] + {:status 200 + :headers {"Content-Type" "image/svg+xml"} + :body (slurp "resources/portal/icon.svg")}) + +(defmethod route [:get "/main.js"] [request] + {:status 200 + :headers {"Content-Type" "text/javascript"} + :body + (slurp + (case (-> request :session :options :mode) + :dev "resources/portal-dev/main.js" + "resources/portal/main.js"))}) + +(defn- get-session-id [request] + ;; There might be a referrer which is not a UUID in standalone mode. + (try + (some-> + (or (:query-string request) + (when-let [referer (get-in request [:headers "referer"])] + (last (str/split referer #"\?")))) + uuid/UUID) + (catch Exception _ nil))) + +(defn- with-session [request] + (if-let [session-id (get-session-id request)] + (assoc request :session (rt/get-session session-id)) + request)) + +(defn- body [{:keys [body headers]}] + (case (get headers "content-type") + "application/json" (json/read body) + "application/cson" (cson/read body) + "application/edn" (edn/read-string body))) + +(defmethod route [:post "/submit"] [request] + (rt/update-value (body request)) + {:status 204 + :headers {"Access-Control-Allow-Origin" "*"}}) + +(defmethod route [:get "/"] [request] + (if-let [session (:session request)] + (send-resource "text/html" (index/html (:options session))) + (let [session-id (random-uuid)] + (swap! rt/sessions assoc session-id {}) + {:status 307 :headers {"Location" (str "?" session-id)}}))) + +(defn- ->headers [request] + (let [headers (.-headers request)] + (persistent! + (reduce + (fn [out header] + (assoc! out (str/lower-case header) (.get headers header))) + (transient {}) + headers)))) + +(defn ^:async handler [request] + (-> {:request-method (keyword (str/lower-case (.-method request))) + :uri (.-path request) + :query-string (not-empty (.-query_string request)) + :headers (->headers request) + :body (when (.-body_exists request) + (await (.text request)))} + with-session + route)) \ No newline at end of file diff --git a/src/portal/runtime/rpc.cljc b/src/portal/runtime/rpc.cljc index 0404d97e..97f99bed 100644 --- a/src/portal/runtime/rpc.cljc +++ b/src/portal/runtime/rpc.cljc @@ -1,8 +1,10 @@ (ns ^:no-doc portal.runtime.rpc (:require #?(:clj [portal.runtime.jvm.client :as c] :cljs [portal.runtime.node.client :as c] - :cljr [portal.runtime.clr.client :as c]) - [portal.runtime :as rt])) + :cljr [portal.runtime.clr.client :as c] + :lpy [portal.runtime.python.client :as c]) + [portal.runtime :as rt] + [portal.runtime.protocols :as p])) (defn on-open [session send!] (swap! rt/connections @@ -40,4 +42,16 @@ (defn on-close [session] (swap! rt/connections dissoc (:session-id session)) - (rt/reset-session session)) \ No newline at end of file + (rt/reset-session session)) + +(defn listener [session] + (reify p/Listener + (on-open [_ socket] + (on-open session (fn send [message] + (p/send socket message)))) + (on-message [_ _socket message] + (on-receive session message)) + (on-close [_ _socket _code _reason] + (on-close session)) + (on-pong [_ _socket _data]) + (on-error [_ _socket _throwable]))) \ No newline at end of file diff --git a/src/portal/runtime/shell.cljc b/src/portal/runtime/shell.cljc index 14ac6636..d211ad5f 100644 --- a/src/portal/runtime/shell.cljc +++ b/src/portal/runtime/shell.cljc @@ -1,7 +1,8 @@ (ns ^:no-doc portal.runtime.shell #?(:clj (:require [clojure.java.shell :as shell]) :cljs (:require ["child_process" :as cp]) - :cljr (:require [clojure.clr.shell :as shell]))) + :cljr (:require [clojure.clr.shell :as shell]) + :lpy (:require [basilisp.shell :as shell]))) (defn spawn [bin & args] #?(:clj @@ -17,6 +18,12 @@ (.on ps "error" reject) (.on ps "close" resolve)))) :cljr + (future + (let [{:keys [exit err out]} (apply shell/sh bin args)] + (when-not (zero? exit) + (prn (into [bin] args)) + (println err out)))) + :lpy (future (let [{:keys [exit err out]} (apply shell/sh bin args)] (when-not (zero? exit) @@ -29,4 +36,5 @@ {:exit (.-status result) :out (str (.-stdout result)) :err (str (.-stderr result))}) - :cljr (apply shell/sh bin args))) \ No newline at end of file + :cljr (apply shell/sh bin args) + :lpy (apply shell/sh bin args))) \ No newline at end of file diff --git a/src/portal/runtime/transit.cljc b/src/portal/runtime/transit.cljc index c1b6e76a..0a2715e7 100644 --- a/src/portal/runtime/transit.cljc +++ b/src/portal/runtime/transit.cljc @@ -11,6 +11,8 @@ (throw (ex-info "transit/read not supported in nbb" {:string string})) :joyride (throw (ex-info "transit/read not supported in joyride" {:string string})) + :lpy + (throw (ex-info "transit/read not supported in basilisp" {:string string})) :clj (-> ^String string .getBytes @@ -24,6 +26,8 @@ (throw (ex-info "transit/write not supported in nbb" {:value value})) :joyride (throw (ex-info "transit/write not supported in joyride" {:value value})) + :lpy + (throw (ex-info "transit/read not supported in basilisp" {:string string})) :clj (let [out (ByteArrayOutputStream. 1024)] (transit/write diff --git a/src/portal/sync.cljc b/src/portal/sync.cljc index 366bb934..f5ced6a3 100644 --- a/src/portal/sync.cljc +++ b/src/portal/sync.cljc @@ -2,6 +2,6 @@ (:refer-clojure :exclude [let try])) (defmacro let [bindings & body] - `(clojure.core/let ~bindings ~@body)) + `(~'let ~bindings ~@body)) (defmacro try [& body] `(~'try ~@body)) diff --git a/src/portal/ui/inspector.cljs b/src/portal/ui/inspector.cljs index 7f065c0f..153c804c 100644 --- a/src/portal/ui/inspector.cljs +++ b/src/portal/ui/inspector.cljs @@ -1,12 +1,10 @@ (ns portal.ui.inspector (:refer-clojure :exclude [coll? map? char?]) - (:require ["anser" :as anser] - [clojure.set :as set] + (:require [clojure.set :as set] [clojure.string :as str] [portal.async :as a] [portal.colors :as c] [portal.runtime.cson :as cson] - [portal.runtime.edn :as edn] [portal.ui.api :as api] [portal.ui.filter :as f] [portal.ui.icons :as icons] @@ -511,7 +509,8 @@ :border-top-right-radius (:border-radius theme) :border-bottom-right-radius 0 :border-bottom-left-radius 0 - :border-bottom :none}} + :border-bottom :none + :align-self :start}} [s/div {:style {:display :flex @@ -899,8 +898,9 @@ :white-space :pre-wrap :font-size (:font-size theme) :font-family (:font-family theme)} - :dangerouslySetInnerHTML - {:__html (anser/ansiToHtml string)}}] + #_#_:dangerouslySetInnerHTML + {:__html (anser/ansiToHtml string)}} + string] (catch :default e (.error js/console e) string)))) @@ -922,25 +922,9 @@ value (trim-string value limit))]])) -(defn- inspect-object* [string] - (let [context (use-context)] - (try - (let [v (edn/read-string string)] - (cond - (nil? v) [highlight-words "nil"] - - (= inspect-object - (get-inspect-component - (get-value-type v))) - [inspect-unreadable string] - - :else [inspector* context v])) - (catch :default _ - [inspect-unreadable string])))) - -(defn- inspect-object [value] [inspect-object* (pr-str value)]) +(defn- inspect-object [value] [inspect-unreadable (pr-str value)]) -(defn- inspect-remote [value] [inspect-object* (:rep value)]) +(defn- inspect-remote [value] [inspect-unreadable (:rep value)]) (defn- get-preview-component [type] (case type diff --git a/src/portal/ui/rpc.cljs b/src/portal/ui/rpc.cljs index 51841b56..5eb38f7d 100644 --- a/src/portal/ui/rpc.cljs +++ b/src/portal/ui/rpc.cljs @@ -54,8 +54,8 @@ (when-not js/goog.DEBUG (extend-type default cson/ToJson - (-to-json [value buffer] - (cson/-to-json + (to-json* [value buffer] + (cson/to-json* (with-meta (cson/tagged-value "remote" (pr-str value)) (meta value)) diff --git a/src/portal/ui/rpc/runtime.cljs b/src/portal/ui/rpc/runtime.cljs index f8856937..96c62ef5 100644 --- a/src/portal/ui/rpc/runtime.cljs +++ b/src/portal/ui/rpc/runtime.cljs @@ -22,7 +22,7 @@ (to-object buffer this :runtime-object nil) (if-let [id (->id this)] (cson/tag buffer "ref" id) - (cson/-to-json + (cson/to-json* (cson/tagged-value "remote" (:pr-str object)) buffer))))) @@ -30,7 +30,7 @@ (deftype RuntimeObject [runtime object] Runtime - cson/ToJson (-to-json [this buffer] (runtime-to-json buffer this)) + cson/ToJson (to-json* [this buffer] (runtime-to-json buffer this)) IMeta (-meta [_] (:meta object)) IHash (-hash [_] (:id object)) IEquiv @@ -59,7 +59,7 @@ (deftype RuntimeAtom [runtime object a] Runtime - cson/ToJson (-to-json [this buffer] (runtime-to-json buffer this)) + cson/ToJson (to-json* [this buffer] (runtime-to-json buffer this)) IAtom IDeref diff --git a/src/portal/ui/viewer/diff.cljs b/src/portal/ui/viewer/diff.cljs index f68ad52c..45757694 100644 --- a/src/portal/ui/viewer/diff.cljs +++ b/src/portal/ui/viewer/diff.cljs @@ -13,13 +13,13 @@ (extend-protocol cson/ToJson diff/Deletion - (-to-json [this buffer] (cson/tag buffer "diff/Deletion" (:- this))) + (to-json* [this buffer] (cson/tag buffer "diff/Deletion" (:- this))) diff/Insertion - (-to-json [this buffer] (cson/tag buffer "diff/Insertion" (:+ this))) + (to-json* [this buffer] (cson/tag buffer "diff/Insertion" (:+ this))) diff/Mismatch - (-to-json [this buffer] (cson/tag buffer "diff/Mismatch" ((juxt :- :+) this)))) + (to-json* [this buffer] (cson/tag buffer "diff/Mismatch" ((juxt :- :+) this)))) (defmethod rpc/-read "diff/Deletion" [_ value] (diff/Deletion. value)) (defmethod rpc/-read "diff/Insertion" [_ value] (diff/Insertion. value)) diff --git a/src/portal/ui/viewer/log.cljs b/src/portal/ui/viewer/log.cljs index a945d2c6..36a8c4cb 100644 --- a/src/portal/ui/viewer/log.cljs +++ b/src/portal/ui/viewer/log.cljs @@ -56,7 +56,10 @@ :icon (inline "runtime/portal.svg")} :joyride {:color ::c/exception :title "Joyride" - :icon (inline "runtime/joyride.svg")}}) + :icon (inline "runtime/joyride.svg")} + :py {:color ::c/tag + :title "Python" + :icon (inline "runtime/python.svg")}}) (defn icon ([value] diff --git a/src/portal/viewer.cljc b/src/portal/viewer.cljc index 55cca2ef..6a69d998 100644 --- a/src/portal/viewer.cljc +++ b/src/portal/viewer.cljc @@ -15,7 +15,10 @@ :org.babashka/nbb (try (with-meta value {}) true (catch :default _e false)) - :cljs (implements? IMeta value))) + :cljs (implements? IMeta value) + :lpy + (try (with-meta value {}) true + (catch Exception _e false)))) (defn default "Set the default viewer for a value. diff --git a/src/examples/data.cljc b/test/examples/data.cljc similarity index 100% rename from src/examples/data.cljc rename to test/examples/data.cljc diff --git a/src/examples/default_visualizer.clj b/test/examples/default_visualizer.clj similarity index 100% rename from src/examples/default_visualizer.clj rename to test/examples/default_visualizer.clj diff --git a/src/examples/fetch.cljs b/test/examples/fetch.cljs similarity index 100% rename from src/examples/fetch.cljs rename to test/examples/fetch.cljs diff --git a/src/examples/hacker_news.cljc b/test/examples/hacker_news.cljc similarity index 100% rename from src/examples/hacker_news.cljc rename to test/examples/hacker_news.cljc diff --git a/src/examples/macros.cljc b/test/examples/macros.cljc similarity index 100% rename from src/examples/macros.cljc rename to test/examples/macros.cljc diff --git a/test/portal/bench.cljc b/test/portal/bench.cljc index 861e2fdb..5c458797 100644 --- a/test/portal/bench.cljc +++ b/test/portal/bench.cljc @@ -1,6 +1,8 @@ (ns portal.bench #?(:cljs (:refer-clojure :exclude [simple-benchmark])) - #?(:cljs (:require-macros portal.bench))) + #?(:cljs (:require-macros portal.bench)) + #?(:lpy (:import [math :as Math] + [time :as time]))) (defn- now ([] @@ -8,14 +10,17 @@ :cljr (.Ticks (System.DateTime/Now)) :cljs (if (exists? js/process) (.hrtime js/process) - (.now js/Date)))) + (.now js/Date)) + :lpy (time/process_time_ns))) ([a] #?(:clj (/ (- (now) a) 1000000.0) :cljr (/ (- (now) a) 10000.0) :cljs (if (exists? js/process) (let [[a b] (.hrtime js/process a)] (+ (* a 1000.0) (/ b 1000000.0))) - (- (.now js/Date) a))))) + (- (.now js/Date) a)) + :lpy (/ (- (now) a) 1000000.0)))) + (defn floor [v] #?(:cljr (Math/Floor v) :default (Math/floor v))) diff --git a/test/portal/client_test.cljc b/test/portal/client_test.cljc index dc244cbd..76b8380a 100644 --- a/test/portal/client_test.cljc +++ b/test/portal/client_test.cljc @@ -16,6 +16,13 @@ [portal.api :as p] [portal.async :as a] [portal.client.node :as c] + [portal.runtime :as rt]) + + :lpy + (:require [clojure.test :refer [deftest is]] + [portal.api :as p] + [portal.sync :as a] + [portal.client.py :as c] [portal.runtime :as rt]))) (def ^:private bad-seq (map (fn [_] (throw (ex-info "Error" {}))) (range 10))) diff --git a/test/portal/runtime/api_test.clj b/test/portal/runtime/api_test.cljc similarity index 69% rename from test/portal/runtime/api_test.clj rename to test/portal/runtime/api_test.cljc index 0c7349a2..bcfc855c 100644 --- a/test/portal/runtime/api_test.clj +++ b/test/portal/runtime/api_test.cljc @@ -1,7 +1,7 @@ (ns portal.runtime.api-test (:require [clojure.test :refer [deftest is]] [portal.api :as p] - [portal.runtime.browser :as browser])) + [portal.runtime.browser :as-alias browser])) (defn- headless-chrome-flags [url] ["--headless=new" "--disable-gpu" "--no-sandbox" url]) @@ -11,18 +11,23 @@ ::browser/chrome-bin ["chromium"] ::browser/flags headless-chrome-flags})) +(defn- test-atom [portal] + (reset! portal 0) + (is (= @portal 0)) + (swap! portal inc) + (is (= @portal 1))) + (deftest e2e-jvm-test (let [portal (open)] - (reset! portal 0) - (is (= @portal 0)) - (swap! portal inc) - (is (= @portal 1)) + #?(:lpy :skip + :default (test-atom portal)) (is (= 6 (p/eval-str portal "(+ 1 2 3)"))) (is (= 6 (p/eval-str portal "*1"))) (is (= :world (:hello (p/eval-str portal "{:hello :world}")))) (is (thrown? - clojure.lang.ExceptionInfo + #?(:lpy Exception :cljs :default :default clojure.lang.ExceptionInfo) (p/eval-str portal "(throw (ex-info \"error\" {:hello :world}))"))) (is (= :hi (p/eval-str portal "(.resolve js/Promise :hi)" {:await true}))) (is (some? (some #{portal} (p/sessions)))) - (p/close portal))) + (p/close portal) + (p/stop))) diff --git a/test/portal/runtime/bench_cson.cljc b/test/portal/runtime/bench_cson.cljc index 8bc13d76..c86a9d6a 100644 --- a/test/portal/runtime/bench_cson.cljc +++ b/test/portal/runtime/bench_cson.cljc @@ -154,4 +154,6 @@ (defn run [] (table (run-benchmark))) -(defn -main [] (p/submit (run-benchmark))) \ No newline at end of file +(defn -main [] (p/submit (run-benchmark))) + +#?(:lpy (-main)) \ No newline at end of file diff --git a/test/portal/runtime/cson_test.cljc b/test/portal/runtime/cson_test.cljc index 4cc473da..7e8a4050 100644 --- a/test/portal/runtime/cson_test.cljc +++ b/test/portal/runtime/cson_test.cljc @@ -58,7 +58,7 @@ 3.14 true false - #inst "2021-04-07T22:43:59.393-00:00" + ;; #inst "2021-04-07T22:43:59.393-00:00" ;; TODO FIXME #uuid "1d80bdbb-ab16-47b2-a8bd-068f94950248" nil 1 @@ -91,7 +91,8 @@ (is (= v (pass v)) "Range with meta works in clj 11 and after") (is (= '() (pass v)) "Range with meta doesn't work in clj 10 or before")) :cljr (is (= v (pass v)) "Range with meta works in cljr") - :cljs (is (= v (pass v)) "Range with meta works in cljs"))) + :cljs (is (= v (pass v)) "Range with meta works in cljs") + :lpy (is (= v (pass v)) "Range with meta works in cljr"))) (let [v (range 0 5 1.0)] (is (= v (pass v)) "Range with no meta works correctly")) (let [v (with-meta (range 0 5 1) {:my :meta})] @@ -105,28 +106,31 @@ #{0} {0 0})) -(deftest sorted-collections - (let [a (sorted-map :a 1 :c 3 :b 2) - b (pass a)] - (is (= a b)) - (is (= (keys a) (keys b))) - (is (= (type a) (type b)))) - (let [a (sorted-map-by > 1 "a" 2 "b" 3 "c") - b (pass a)] - (is (= a b)) - (is (= (keys a) (keys b))) - (is (= (type a) (type b)))) - (let [a (sorted-set 1 2 3) - b (pass a)] - (is (= a b)) - (is (= (seq a) (seq b)))) - (let [a (sorted-set-by > 1 2 3) - b (pass a)] - (is (= a b)) - (is (= (seq a) (seq b))))) +#?(:lpy :skip + :default + (deftest sorted-collections + (let [a (sorted-map :a 1 :c 3 :b 2) + b (pass a)] + (is (= a b)) + (is (= (keys a) (keys b))) + (is (= (type a) (type b)))) + (let [a (sorted-map-by > 1 "a" 2 "b" 3 "c") + b (pass a)] + (is (= a b)) + (is (= (keys a) (keys b))) + (is (= (type a) (type b)))) + (let [a (sorted-set 1 2 3) + b (pass a)] + (is (= a b)) + (is (= (seq a) (seq b)))) + (let [a (sorted-set-by > 1 2 3) + b (pass a)] + (is (= a b)) + (is (= (seq a) (seq b)))))) (def tagged - [#inst "2021-04-07T22:43:59.393-00:00" + [#?(:lpy :skip + :default #inst "2021-04-07T22:43:59.393-00:00") #?(:clj (UUID/randomUUID) :cljr (Guid/NewGuid) :cljs (random-uuid)) @@ -146,7 +150,7 @@ (are [v] (= v (pass v)) v1 v2) (are [v] (= (meta* v) (meta* (pass v))) v1 v2)) (is (thrown? - #?(:clj AssertionError :cljr Exception :cljs js/Error) + #?(:clj AssertionError :cljr Exception :cljs js/Error :lpy Exception) (cson/tagged-value :my/tag {:hello :world})) "only allow string tags")) @@ -197,5 +201,7 @@ (deftest binary (let [bin #?(:clj (.getBytes "hi") :cljr (.GetBytes Encoding/UTF8 "hi") - :cljs (.encode (js/TextEncoder.) "hi"))] - (is (= "[\"bin\",\"aGk=\"]" (cson/write bin))))) \ No newline at end of file + :cljs (.encode (js/TextEncoder.) "hi") + :default nil)] + (when bin + (is (= "[\"bin\",\"aGk=\"]" (cson/write bin)))))) \ No newline at end of file diff --git a/test/portal/runtime/npm_test.cljc b/test/portal/runtime/npm_test.cljc index 22d45b12..979ebb44 100644 --- a/test/portal/runtime/npm_test.cljc +++ b/test/portal/runtime/npm_test.cljc @@ -12,6 +12,6 @@ (deftest invalid-modules (are [module] (thrown? - #?(:clj Exception :cljr Exception :cljs js/Error) + #?(:clj Exception :cljr Exception :cljs js/Error :lpy Exception) (node-resolve module)) "react/index.j")) diff --git a/test/portal/runtime_test.cljc b/test/portal/runtime_test.cljc index c42f72f8..90ee1b23 100644 --- a/test/portal/runtime_test.cljc +++ b/test/portal/runtime_test.cljc @@ -47,7 +47,7 @@ [1] '(1) - #{1 2 3} (sorted-set 1 2 3) + #{1 2 3} #?(:lpy :skip :default (sorted-set 1 2 3)) ^{:one 1} [] ^{:two 2} [] diff --git a/test/portal/test_runner.lpy b/test/portal/test_runner.lpy new file mode 100644 index 00000000..1490d43a --- /dev/null +++ b/test/portal/test_runner.lpy @@ -0,0 +1,63 @@ +(ns portal.test-runner + (:require [clojure.test :as t] + [clojure.string :as str] + [portal.runtime.fs :as fs]) + (:import [sys :as sys] + [os :as os])) + +(defn- load-test [ns] + @(future + (load-file + (str + (fs/join (fs/cwd) + "test" + (str/join "/" (str/split (str/replace (name ns) #"-" "_") #"\\."))) + ".cljc")))) + +(defn- ->source [v] + (let [{:keys [file line col]} (meta v)] + (str file ":" line ":" col))) + +(defn- run-tests [& tests] + (let [summary (atom {:error 0 :fail 0}) + report (atom [])] + (doseq [test-ns tests] + (swap! report conj {:type :begin-test-ns :ns test-ns}) + (load-test test-ns) + (println "\nTesting" (name test-ns)) + (doseq [[_s v] (ns-publics test-ns) + :when (::t/test (meta v))] + (swap! report conj {:type :begin-test-var :var v}) + (try + (let [failures (:failures (v))] + (swap! summary update :fail + (count failures)) + (doseq [failure failures] + (println "\nFAIL in" (str "(" (name v) ")") (->source v)) + (println "expected:" (:expected failure)) + (println " actual:" (:actual failure)) + (swap! report conj (merge {:type :fail} (select-keys failure [:message :actual :expected]))))) + (catch Exception e + (swap! report conj (merge {:type :error} {:message (ex-message e) :data (ex-data e)})) + (swap! summary update :error inc) + e)) + (swap! report conj {:type :end-test-var :var v})) + (swap! report conj {:type :end-test-ns :ns test-ns})) + (println "\nRan all tests.") + (println (:fail @summary) "failures" (:error @summary) "errors.") + #_(prn @report) + @summary)) + +(defn -main [] + (let [{:keys [fail error]} + (run-tests + 'portal.runtime.cson-test + 'portal.runtime.fs-test + 'portal.runtime.json-buffer-test + 'portal.runtime.npm-test + 'portal.runtime.shell-test + 'portal.client-test + 'portal.runtime-test + 'portal.runtime.api-test)] + (sys/exit (+ fail error)))) + +(-main) \ No newline at end of file From 0c11631b503cc110b1ebf3c01b04098bea5214b6 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sun, 20 Jul 2025 00:26:32 -0700 Subject: [PATCH 02/15] cleanup --- {test => src}/examples/data.cljc | 0 {test => src}/examples/default_visualizer.clj | 0 {test => src}/examples/fetch.cljs | 0 {test => src}/examples/hacker_news.cljc | 0 {test => src}/examples/macros.cljc | 0 src/portal/runtime/cson.cljc | 41 +------------------ 6 files changed, 1 insertion(+), 40 deletions(-) rename {test => src}/examples/data.cljc (100%) rename {test => src}/examples/default_visualizer.clj (100%) rename {test => src}/examples/fetch.cljs (100%) rename {test => src}/examples/hacker_news.cljc (100%) rename {test => src}/examples/macros.cljc (100%) diff --git a/test/examples/data.cljc b/src/examples/data.cljc similarity index 100% rename from test/examples/data.cljc rename to src/examples/data.cljc diff --git a/test/examples/default_visualizer.clj b/src/examples/default_visualizer.clj similarity index 100% rename from test/examples/default_visualizer.clj rename to src/examples/default_visualizer.clj diff --git a/test/examples/fetch.cljs b/src/examples/fetch.cljs similarity index 100% rename from test/examples/fetch.cljs rename to src/examples/fetch.cljs diff --git a/test/examples/hacker_news.cljc b/src/examples/hacker_news.cljc similarity index 100% rename from test/examples/hacker_news.cljc rename to src/examples/hacker_news.cljc diff --git a/test/examples/macros.cljc b/src/examples/macros.cljc similarity index 100% rename from test/examples/macros.cljc rename to src/examples/macros.cljc diff --git a/src/portal/runtime/cson.cljc b/src/portal/runtime/cson.cljc index 347708eb..8f7f3ba4 100644 --- a/src/portal/runtime/cson.cljc +++ b/src/portal/runtime/cson.cljc @@ -1017,46 +1017,7 @@ (binding [*to-json* to-json-proto] (to-json buffer value)) - :else (throw (ex-info "Unknown value type" {:value value})))) - #_(cond - (tagged-value? value) - (push-tagged buffer value) - - (nil? value) (json/push-null buffer) - (boolean? value) (json/push-bool buffer value) - (is-char? value) (push-char buffer value) - (string? value) (push-string buffer value) - (bigint? value) (push-bigint buffer value) - - (number? value) (cond - (float? value) (push-double buffer value) - :else (box-long buffer value)) - - (keyword? value) (push-keyword buffer value) - (symbol? value) (push-symbol buffer value) - - (map? value) (cond - (sorted? value) (tagged-map buffer "smap" value) - :else (tagged-map buffer value)) - - (vector? value) (tagged-coll buffer "[" value) - (set? value) (cond - (sorted? value) (tagged-coll buffer "sset" value) - :else (tagged-coll buffer "#" value)) - (coll? value) (cond - (range? value) (tagged-coll buffer "(" (into [] value)) - :else (tagged-coll buffer "(" value)) - (uuid? value) (push-uuid buffer value) - (inst? value) (push-inst buffer value) - - (tagged-literal? value) - (push-tagged-literal buffer value) - - (nil? (::dispatch *options*)) - (binding [*to-json* to-json-proto] - (to-json buffer value)) - - :else (throw (ex-info "Unknown value type" {:value value})))) + :else (throw (ex-info "Unknown value type" {:value value}))))) (defn write ([value] (write value nil)) From 3256c6b82be87f3f05a59c257552d54afc3afadf Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sun, 20 Jul 2025 10:29:04 -0700 Subject: [PATCH 03/15] Improve test tooling --- .github/workflows/clojure.yml | 1 + src/portal/client/py.lpy | 6 ++++-- test/portal/client.cljc | 2 +- test/portal/test_runner.lpy | 13 ++++++++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/clojure.yml b/.github/workflows/clojure.yml index bc47ed80..c0479b16 100644 --- a/.github/workflows/clojure.yml +++ b/.github/workflows/clojure.yml @@ -123,6 +123,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.13' + cache: 'pip' - run: pip install virtualenv - run: bb -m tasks.test/lpy app: diff --git a/src/portal/client/py.lpy b/src/portal/client/py.lpy index a4f035b8..712e7a20 100644 --- a/src/portal/client/py.lpy +++ b/src/portal/client/py.lpy @@ -1,5 +1,6 @@ (ns portal.client.py - (:require [basilisp.json :as json]) + (:require [basilisp.json :as json] + [portal.runtime.cson :as cson]) (:import [urllib.request :as request])) (defn- serialize [encoding value] @@ -8,7 +9,8 @@ (case encoding :json (json/write-str value) :edn (binding [*print-meta* true] - (pr-str value))) + (pr-str value)) + :cson (cson/write value)) "utf-8") (catch Exception ex (serialize diff --git a/test/portal/client.cljc b/test/portal/client.cljc index 5e4a30d5..7fc6ee25 100644 --- a/test/portal/client.cljc +++ b/test/portal/client.cljc @@ -2,7 +2,7 @@ #?(:clj (:require [portal.client.jvm :as p]) :cljr (:require [portal.client.clr :as p]) :cljs (:require [portal.client.node :as p]) - :lpy (:require [portal.client.python :as p])) + :lpy (:require [portal.client.py :as p])) #?(:cljr (:import (System Environment)) :lpy (:import [os :as os]))) diff --git a/test/portal/test_runner.lpy b/test/portal/test_runner.lpy index 1490d43a..72c1daa9 100644 --- a/test/portal/test_runner.lpy +++ b/test/portal/test_runner.lpy @@ -1,10 +1,17 @@ (ns portal.test-runner - (:require [clojure.test :as t] - [clojure.string :as str] + (:require [clojure.string :as str] + [clojure.test :as t] + [portal.client.py :as p] [portal.runtime.fs :as fs]) (:import [sys :as sys] [os :as os])) +(defn- get-port [] (.get os/environ "PORTAL_PORT")) + +(defn- submit [value] + (when-let [port (get-port)] + (p/submit {:port port :encoding :cson} value))) + (defn- load-test [ns] @(future (load-file @@ -44,7 +51,7 @@ (swap! report conj {:type :end-test-ns :ns test-ns})) (println "\nRan all tests.") (println (:fail @summary) "failures" (:error @summary) "errors.") - #_(prn @report) + (submit @report) @summary)) (defn -main [] From 5536e180c2dbe27681d3e6bfc814d5d871537044 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sun, 20 Jul 2025 10:40:18 -0700 Subject: [PATCH 04/15] Revert inspector changes --- src/portal/ui/inspector.cljs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/portal/ui/inspector.cljs b/src/portal/ui/inspector.cljs index 153c804c..fe716732 100644 --- a/src/portal/ui/inspector.cljs +++ b/src/portal/ui/inspector.cljs @@ -1,10 +1,12 @@ (ns portal.ui.inspector (:refer-clojure :exclude [coll? map? char?]) - (:require [clojure.set :as set] + (:require ["anser" :as anser] + [clojure.set :as set] [clojure.string :as str] [portal.async :as a] [portal.colors :as c] [portal.runtime.cson :as cson] + [portal.runtime.edn :as edn] [portal.ui.api :as api] [portal.ui.filter :as f] [portal.ui.icons :as icons] @@ -898,9 +900,8 @@ :white-space :pre-wrap :font-size (:font-size theme) :font-family (:font-family theme)} - #_#_:dangerouslySetInnerHTML - {:__html (anser/ansiToHtml string)}} - string] + :dangerouslySetInnerHTML + {:__html (anser/ansiToHtml string)}}] (catch :default e (.error js/console e) string)))) @@ -922,9 +923,25 @@ value (trim-string value limit))]])) -(defn- inspect-object [value] [inspect-unreadable (pr-str value)]) +(defn- inspect-object* [string] + (let [context (use-context)] + (try + (let [v (edn/read-string string)] + (cond + (nil? v) [highlight-words "nil"] + + (= inspect-object + (get-inspect-component + (get-value-type v))) + [inspect-unreadable string] + + :else [inspector* context v])) + (catch :default _ + [inspect-unreadable string])))) + +(defn- inspect-object [value] [inspect-object* (pr-str value)]) -(defn- inspect-remote [value] [inspect-unreadable (:rep value)]) +(defn- inspect-remote [value] [inspect-object* (:rep value)]) (defn- get-preview-component [type] (case type From 0e5e03260f1987dd742f04dc09ad5aa82929f999 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sun, 20 Jul 2025 11:59:17 -0700 Subject: [PATCH 05/15] Include basilisp in `bb bench` --- dev/tasks/bench.clj | 12 +++++------ src/examples/data.cljc | 26 +++++++++++++++++------ src/examples/macros.cljc | 7 ++++-- {test => src}/portal/client.cljc | 9 +------- src/portal/runtime/transit.cljc | 2 +- test/portal/{runtime => }/bench_cson.cljc | 3 ++- test/portal/test_runner.lpy | 10 ++------- 7 files changed, 37 insertions(+), 32 deletions(-) rename {test => src}/portal/client.cljc (72%) rename test/portal/{runtime => }/bench_cson.cljc (99%) diff --git a/dev/tasks/bench.clj b/dev/tasks/bench.clj index 5ea5052c..8b85809c 100644 --- a/dev/tasks/bench.clj +++ b/dev/tasks/bench.clj @@ -1,7 +1,7 @@ (ns tasks.bench (:require [portal.api :as p] - [portal.runtime.bench-cson :as bc] + [portal.bench-cson :as bc] [tasks.parallel :refer [with-out-data]] [tasks.tools :as t])) @@ -12,11 +12,11 @@ #(let [{:keys [tag val]} %] (when (= :tap tag) val)) @f)) - (for [f [#(t/clj "-M:test" "-m" :portal.runtime.bench-cson) - #(t/bb "-m" :portal.runtime.bench-cson) - #(t/cljr "-m" :portal.runtime.bench-cson) - #(t/nbb "-m" :portal.runtime.bench-cson) - #_#(t/lpy :run "-n" :portal.runtime.bench-cson)]] + (for [f [#(t/clj "-M:test" "-m" :portal.bench-cson) + #(t/bb "-m" :portal.bench-cson) + #(t/cljr "-m" :portal.bench-cson) + #(t/nbb "-m" :portal.bench-cson) + #(t/lpy :run "-n" :portal.bench-cson)]] (future (with-out-data (f)))))) (def windows diff --git a/src/examples/data.cljc b/src/examples/data.cljc index 7568aec5..be9a9989 100644 --- a/src/examples/data.cljc +++ b/src/examples/data.cljc @@ -1,7 +1,9 @@ (ns examples.data (:require #?(:clj [clojure.java.io :as io]) #?(:org.babashka/nbb [clojure.core] - :default [examples.hacker-news :as hn]) + :clj [examples.hacker-news :as hn] + :cljr [examples.hacker-news :as hn] + :cljs [examples.hacker-news :as hn]) [clojure.pprint :as pp] [examples.macros :refer [read-file]] [portal.colors :as c] @@ -13,7 +15,8 @@ :org.babashka/nbb (:import) :cljs (:import [goog.math Long]) :cljr (:import [System DateTime Guid Uri] - [System.IO File]))) + [System.IO File]) + :lpy (:import [math :as Math]))) #?(:clj (defn slurp-bytes [x] @@ -66,7 +69,14 @@ ::date (js/Date.) ::bigint (js/BigInt "42") ::js-array #js [0 1 2 3 4] - ::js-object #js {:hello "world"}})) + ::js-object #js {:hello "world"}} + :lpy + {::class (type {}) + ::ratio 22/7 + ::uuid #uuid "844d415a-5288-4c2c-a163-0d104e899fa8" + ::date #inst "2021-04-07T22:43:59.393-00:00" + ::array #py [0 1 2 3 4] + ::hash #py {:hello "world"}})) (def platform-collections #?(:bb nil @@ -107,8 +117,10 @@ (def clojure-data {::regex #"hello-world" - ::sorted-map (sorted-map-by gt 3 "c" 2 "b" 1 "a") - ::sorted-set (sorted-set-by gt 3 2 1) + ::sorted-map #?(:lpy :not-implemented + :default (sorted-map-by gt 3 "c" 2 "b" 1 "a")) + ::sorted-set #?(:lpy :not-implemented + :default (sorted-set-by gt 3 2 1)) ::var #'portal.colors/themes ::with-meta (with-meta 'with-meta {:hello :world}) ::tagged (tagged-literal 'my/tag ["hello, world"]) @@ -870,6 +882,8 @@ {:columns [:a :b :c :d :e]}) ::multi-map map-reflection-data}) +(declare test-report) + (def test-report (v/test-report [{:type :begin-test-ns @@ -1123,7 +1137,7 @@ (def data (merge {::platform-data platform-data - ::hacker-news #?(:org.babashka/nbb nil :default hn/stories) + ::hacker-news #?(:org.babashka/nbb nil :lpy :not-implemented :default hn/stories) ::spec-data spec-data ::table-data table-data ::diff diff-data diff --git a/src/examples/macros.cljc b/src/examples/macros.cljc index 88909db7..6ddfd229 100644 --- a/src/examples/macros.cljc +++ b/src/examples/macros.cljc @@ -1,6 +1,9 @@ (ns ^:no-doc examples.macros - #?(:portal (:import) :cljs (:require-macros examples.macros))) + #?(:portal (:import) + :cljs (:require-macros examples.macros) + :lpy (:require [portal.runtime.fs :as fs]))) #?(:clj (defmacro read-file [file-name] (slurp file-name)) :cljs (defn read-file [_file-name] ::missing) - :cljr (defmacro read-file [file-name] (slurp file-name :enc "utf8"))) + :cljr (defmacro read-file [file-name] (slurp file-name :enc "utf8")) + :lpy (defn read-file [file-name] (fs/slurp file-name))) diff --git a/test/portal/client.cljc b/src/portal/client.cljc similarity index 72% rename from test/portal/client.cljc rename to src/portal/client.cljc index 7fc6ee25..afd99fd2 100644 --- a/test/portal/client.cljc +++ b/src/portal/client.cljc @@ -15,11 +15,4 @@ (defn enabled? [] (some? port)) (defn submit [value] - (p/submit {:port port :encoding :cson} value)) - -;; (defn table [value] -;; (if (enabled?) -;; (submit value) -;; (pp/print-table -;; (get-in (meta value) [:portal.viewer/table :columns]) -;; value))) \ No newline at end of file + (p/submit {:port port :encoding :cson} value)) \ No newline at end of file diff --git a/src/portal/runtime/transit.cljc b/src/portal/runtime/transit.cljc index 0a2715e7..714145a8 100644 --- a/src/portal/runtime/transit.cljc +++ b/src/portal/runtime/transit.cljc @@ -27,7 +27,7 @@ :joyride (throw (ex-info "transit/write not supported in joyride" {:value value})) :lpy - (throw (ex-info "transit/read not supported in basilisp" {:string string})) + (throw (ex-info "transit/read not supported in basilisp" {:value value})) :clj (let [out (ByteArrayOutputStream. 1024)] (transit/write diff --git a/test/portal/runtime/bench_cson.cljc b/test/portal/bench_cson.cljc similarity index 99% rename from test/portal/runtime/bench_cson.cljc rename to test/portal/bench_cson.cljc index c86a9d6a..00057818 100644 --- a/test/portal/runtime/bench_cson.cljc +++ b/test/portal/bench_cson.cljc @@ -1,4 +1,4 @@ -(ns portal.runtime.bench-cson +(ns portal.bench-cson (:require [clojure.edn :as edn] [examples.data :as d] [portal.bench :as b] @@ -28,6 +28,7 @@ (def ^:private formats #?(:org.babashka/nbb [:edn :cson] :cljr [:edn :cson] + :lpy [:cson] :default [:transit :edn :cson])) (defn run-benchmark [] diff --git a/test/portal/test_runner.lpy b/test/portal/test_runner.lpy index 72c1daa9..85b82e33 100644 --- a/test/portal/test_runner.lpy +++ b/test/portal/test_runner.lpy @@ -1,17 +1,11 @@ (ns portal.test-runner (:require [clojure.string :as str] [clojure.test :as t] - [portal.client.py :as p] + [portal.client :as p] [portal.runtime.fs :as fs]) (:import [sys :as sys] [os :as os])) -(defn- get-port [] (.get os/environ "PORTAL_PORT")) - -(defn- submit [value] - (when-let [port (get-port)] - (p/submit {:port port :encoding :cson} value))) - (defn- load-test [ns] @(future (load-file @@ -51,7 +45,7 @@ (swap! report conj {:type :end-test-ns :ns test-ns})) (println "\nRan all tests.") (println (:fail @summary) "failures" (:error @summary) "errors.") - (submit @report) + (when (p/enabled?) (p/submit @report)) @summary)) (defn -main [] From a24639bd7689596ed4fb571419b23ace964b4e59 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sun, 20 Jul 2025 13:21:36 -0700 Subject: [PATCH 06/15] Include cljs in `bb bench` --- dev/tasks/bench.clj | 1 + dev/tasks/tools.clj | 21 ++++++- src/portal/runtime/cson.cljc | 111 ++++------------------------------- test/portal/bench_cson.cljc | 2 +- 4 files changed, 34 insertions(+), 101 deletions(-) diff --git a/dev/tasks/bench.clj b/dev/tasks/bench.clj index 8b85809c..57ec9f7c 100644 --- a/dev/tasks/bench.clj +++ b/dev/tasks/bench.clj @@ -14,6 +14,7 @@ @f)) (for [f [#(t/clj "-M:test" "-m" :portal.bench-cson) #(t/bb "-m" :portal.bench-cson) + #(t/cljs "1.10.773" :portal.bench-cson) #(t/cljr "-m" :portal.bench-cson) #(t/nbb "-m" :portal.bench-cson) #(t/lpy :run "-n" :portal.bench-cson)]] diff --git a/dev/tasks/tools.clj b/dev/tasks/tools.clj index b27df2fa..2f49a6fb 100644 --- a/dev/tasks/tools.clj +++ b/dev/tasks/tools.clj @@ -1,5 +1,6 @@ (ns tasks.tools - (:require [babashka.process :as p] + (:require [babashka.fs :as fs] + [babashka.process :as p] [clojure.java.io :as io] [clojure.string :as str] [io.aviso.ansi :as a]) @@ -100,3 +101,21 @@ :extra-env {"PYTHONPATH" "src:test"})] (apply sh "./target/py/bin/basilisp" args))) + +(defn cljs [version main] + (let [deps {'org.clojure/clojurescript {:mvn/version version}} + out (str "target/" (name main) "." version)] + (when (seq + (fs/modified-since + out + (concat + (fs/glob "src" "**") + (fs/glob "test" "**")))) + (clj "-Sdeps" (pr-str {:deps deps}) + "-M:test" + "-m" :cljs.main + "--output-dir" out + "--target" :node + "--output-to" (str out ".js") + "--compile" main)) + (node out))) \ No newline at end of file diff --git a/src/portal/runtime/cson.cljc b/src/portal/runtime/cson.cljc index 8f7f3ba4..eda9f511 100644 --- a/src/portal/runtime/cson.cljc +++ b/src/portal/runtime/cson.cljc @@ -34,19 +34,13 @@ (declare ->value) (defonce ^:dynamic *options* nil) -(defonce ^:private ^:dynamic *to-json* nil) (defn- transform [value] (if-let [f (:transform *options*)] (f value) value)) -(defn- to-json [buffer value] (*to-json* buffer (transform value))) - -(defn- get-to-json [] - (let [to-json-capture *to-json*] - (fn [buffer value] - (to-json-capture buffer (transform value))))) +(defn- to-json [buffer value] (to-json* (transform value) buffer)) (defn tag [buffer tag value] (assert tag string?) @@ -319,12 +313,6 @@ (defn- ->bin [buffer] (base64-decode (json/next-string buffer))) -(defn- bigint? [value] - #?(:clj (instance? clojure.lang.BigInt value) - :cljr (instance? clojure.lang.BigInt value) - :cljs (identical? js/BigInt (some-> value .-constructor)) - :else false)) - (defn push-bigint [buffer value] (-> buffer (json/push-string "N") @@ -387,13 +375,6 @@ 13 "return" (.fromCharCode js/String code)))))) -(defn- is-char? [value] - #?(:joyride false - :org.babashka/nbb false - :cljs (instance? Character value) - :lpy false - :default (char? value))) - (defn- ->char [buffer] #?(:clj (char (->value buffer)) :cljr (char (->value buffer)) @@ -536,7 +517,7 @@ (tagged-coll buffer tag (meta value) value)) ([buffer tag meta-map value] (reduce - (get-to-json) + to-json (-> buffer (push-meta meta-map) (json/push-string tag) @@ -775,17 +756,16 @@ ([buffer tag value] (tagged-map buffer tag (meta value) value)) ([buffer tag meta-map value] - (let [f (get-to-json)] - (reduce-kv - (fn [buffer k v] - (-> buffer - (f k) - (f v))) + (reduce-kv + (fn [buffer k v] (-> buffer - (push-meta meta-map) - (json/push-string tag) - (json/push-long (count value))) - value)))) + (to-json k) + (to-json v))) + (-> buffer + (push-meta meta-map) + (json/push-string tag) + (json/push-long (count value))) + value))) #?(:joyride (def PersistentHashMap (type (hash-map)))) #?(:org.babashka/nbb (def PersistentHashMap (type (hash-map)))) @@ -953,79 +933,12 @@ (let [handler (:default-handler *options* tagged-value)] (handler op (->value buffer)))))))) -(defn- to-json-proto [buffer value] (to-json* value buffer)) - -(defn- range? [value] - #?(:clj (instance? clojure.lang.Range value) - :cljr (instance? clojure.lang.Range value) - :org.babashka/nbb false - :cljs (instance? Range value))) - #?(:lpy (def sorted? (constantly false))) -(defn- to-json-cond [buffer value] - (if (coll? value) - (cond - (tagged-value? value) - (push-tagged buffer value) - - (map? value) #?(:lpy (tagged-map buffer value) - :default - (cond - (sorted? value) (tagged-map buffer "smap" value) - :else (tagged-map buffer value))) - - (vector? value) (tagged-coll buffer "[" value) - (set? value) #?(:lpy (tagged-coll buffer "#" value) - :default - (cond - (sorted? value) (tagged-coll buffer "sset" value) - :else (tagged-coll buffer "#" value))) - (coll? value) #?(:lpy (tagged-coll buffer "(" value) - :default - (cond - (range? value) (tagged-coll buffer "(" (into [] value)) - :else (tagged-coll buffer "(" value))) - - (nil? (::dispatch *options*)) - (binding [*to-json* to-json-proto] - (to-json buffer value)) - - :else (throw (ex-info "Unknown value type" {:value value}))) - (cond - (number? value) (cond - (float? value) (push-double buffer value) - :else (box-long buffer value)) - - (string? value) (push-string buffer value) - (boolean? value) (json/push-bool buffer value) - (nil? value) (json/push-null buffer) - - (keyword? value) (push-keyword buffer value) - (symbol? value) (push-symbol buffer value) - - (bigint? value) (push-bigint buffer value) - (uuid? value) (push-uuid buffer value) - (inst? value) (push-inst buffer value) - - (tagged-literal? value) - (push-tagged-literal buffer value) - - (is-char? value) (push-char buffer value) - - (nil? (::dispatch *options*)) - (binding [*to-json* to-json-proto] - (to-json buffer value)) - - :else (throw (ex-info "Unknown value type" {:value value}))))) - (defn write ([value] (write value nil)) ([value options] - (binding [*options* options - *to-json* (case (::dispatch options :prototype) - :prototype to-json-proto - :cond to-json-cond)] + (binding [*options* options] (json/with-buffer to-json value)))) (defn read diff --git a/test/portal/bench_cson.cljc b/test/portal/bench_cson.cljc index 00057818..00a275c9 100644 --- a/test/portal/bench_cson.cljc +++ b/test/portal/bench_cson.cljc @@ -157,4 +157,4 @@ (defn -main [] (p/submit (run-benchmark))) -#?(:lpy (-main)) \ No newline at end of file +#?(:lpy (-main) :org.babashka/nbb :skip :cljs (-main)) \ No newline at end of file From 6158d7e691020a9aa9faad52955b6c232e5d88fa Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 16:33:42 -0700 Subject: [PATCH 07/15] Fix basilisp fs/slurp encoding --- src/portal/runtime/fs.cljc | 2 +- src/portal/runtime/python/server.lpy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/portal/runtime/fs.cljc b/src/portal/runtime/fs.cljc index 863b6ae8..bf778af8 100644 --- a/src/portal/runtime/fs.cljc +++ b/src/portal/runtime/fs.cljc @@ -23,7 +23,7 @@ #?(:clj (clojure.core/slurp path) :cljs (fs/readFileSync path "utf8") :cljr (clojure.core/slurp path :enc "utf8") - :lpy (basilisp.core/slurp path))) + :lpy (basilisp.core/slurp path :encoding "utf8"))) (defn spit [path content] #?(:clj (clojure.core/spit path content) diff --git a/src/portal/runtime/python/server.lpy b/src/portal/runtime/python/server.lpy index ab798ae4..5e5e207a 100644 --- a/src/portal/runtime/python/server.lpy +++ b/src/portal/runtime/python/server.lpy @@ -33,7 +33,7 @@ {:status 200 :headers {"Content-Type" "text/javascript"} :body - (slurp + (fs/slurp (case (-> request :session :options :mode) :dev "resources/portal-dev/main.js" "resources/portal/main.js"))}) From 6a1b5724fea470414e315f2c08ca0cc493d26854 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 16:34:29 -0700 Subject: [PATCH 08/15] Fix basilisp jack-in on windows --- dev/tasks/py.clj | 2 +- dev/tasks/tools.clj | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/dev/tasks/py.clj b/dev/tasks/py.clj index 779cd384..77e384f4 100644 --- a/dev/tasks/py.clj +++ b/dev/tasks/py.clj @@ -8,7 +8,7 @@ (py "-m" :venv "target/py") (pip :install "-r" "requirements.txt"))) -(defn nrepl [] (lpy :nrepl-server)) +(defn nrepl [] (lpy :nrepl-server "--include-path" "src")) (defn -main "Start basilisp dev env / nrepl" diff --git a/dev/tasks/tools.clj b/dev/tasks/tools.clj index 2f49a6fb..df7afd71 100644 --- a/dev/tasks/tools.clj +++ b/dev/tasks/tools.clj @@ -91,8 +91,14 @@ ["src" "resources" "dev" "test"]))] (apply sh :Clojure.Main args))) -(def py (partial #'sh :python3)) -(def pip (partial #'sh "./target/py/bin/pip")) +(defn- py-script [bin] + (str (if windows? + "./target/py/Scripts/" + "./target/py/bin/") + (name bin))) + +(def py (partial #'sh :python3)) +(def pip (partial #'sh (py-script :pip))) (defn lpy [& args] (binding [*opts* @@ -100,7 +106,7 @@ :inherit true :extra-env {"PYTHONPATH" "src:test"})] - (apply sh "./target/py/bin/basilisp" args))) + (apply sh (py-script :basilisp) args))) (defn cljs [version main] (let [deps {'org.clojure/clojurescript {:mvn/version version}} From f5e9f1c1fa2d6846375df954b89802dba896bbcf Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 16:49:25 -0700 Subject: [PATCH 09/15] Try CI fix --- test/portal/client_test.cljc | 1 - test/portal/runtime/api_test.cljc | 3 +-- test/portal/test_clr.clj | 2 ++ test/portal/test_runner.clj | 2 ++ test/portal/test_runner.lpy | 5 +++-- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/portal/client_test.cljc b/test/portal/client_test.cljc index 76b8380a..6f81430a 100644 --- a/test/portal/client_test.cljc +++ b/test/portal/client_test.cljc @@ -34,7 +34,6 @@ (p/start opts) (c/submit opts ::value) (c/submit opts bad-seq) - (p/stop) (is (= "Error" (:cause (first @tap-list)))) (is (= ::value (second @tap-list))) (done))) diff --git a/test/portal/runtime/api_test.cljc b/test/portal/runtime/api_test.cljc index bcfc855c..b1c14aef 100644 --- a/test/portal/runtime/api_test.cljc +++ b/test/portal/runtime/api_test.cljc @@ -29,5 +29,4 @@ (p/eval-str portal "(throw (ex-info \"error\" {:hello :world}))"))) (is (= :hi (p/eval-str portal "(.resolve js/Promise :hi)" {:await true}))) (is (some? (some #{portal} (p/sessions)))) - (p/close portal) - (p/stop))) + (p/close portal))) diff --git a/test/portal/test_clr.clj b/test/portal/test_clr.clj index e46e5df2..1f0e7e45 100644 --- a/test/portal/test_clr.clj +++ b/test/portal/test_clr.clj @@ -1,5 +1,6 @@ (ns portal.test-clr (:require [clojure.test :as t] + [portal.api :as api] [portal.client :as p] [portal.client-test] [portal.runtime-test] @@ -34,5 +35,6 @@ 'portal.runtime.json-buffer-test 'portal.runtime.npm-test 'portal.runtime.shell-test)] + (api/stop) (shutdown-agents) (Environment/Exit (+ fail error)))) diff --git a/test/portal/test_runner.clj b/test/portal/test_runner.clj index 37e9f025..5f50d1b4 100644 --- a/test/portal/test_runner.clj +++ b/test/portal/test_runner.clj @@ -1,5 +1,6 @@ (ns portal.test-runner (:require [clojure.test :as t] + [portal.api :as api] [portal.client :as p] [portal.client-test] [portal.runtime-test] @@ -34,5 +35,6 @@ 'portal.runtime.jvm.editor-test 'portal.runtime.npm-test 'portal.runtime.shell-test)] + (api/stop) (shutdown-agents) (System/exit (+ fail error)))) diff --git a/test/portal/test_runner.lpy b/test/portal/test_runner.lpy index 85b82e33..f32c7615 100644 --- a/test/portal/test_runner.lpy +++ b/test/portal/test_runner.lpy @@ -1,10 +1,10 @@ (ns portal.test-runner (:require [clojure.string :as str] [clojure.test :as t] + [portal.api :as api] [portal.client :as p] [portal.runtime.fs :as fs]) - (:import [sys :as sys] - [os :as os])) + (:import [sys :as sys])) (defn- load-test [ns] @(future @@ -59,6 +59,7 @@ 'portal.client-test 'portal.runtime-test 'portal.runtime.api-test)] + (api/stop) (sys/exit (+ fail error)))) (-main) \ No newline at end of file From d8b5be4634af38f02ae7ede6a6e75ddf44e00525 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 17:18:02 -0700 Subject: [PATCH 10/15] Fix fs/dirname for basilisp on windows --- src/portal/runtime/fs.cljc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/portal/runtime/fs.cljc b/src/portal/runtime/fs.cljc index bf778af8..d19f9ec0 100644 --- a/src/portal/runtime/fs.cljc +++ b/src/portal/runtime/fs.cljc @@ -137,8 +137,8 @@ :cljs (let [root (.-root (path/parse path))] (when-not (= path root) (path/dirname path))) :cljr (some-> (Directory/GetParent path) str) - :lpy (when-not (= "/" path) - (os.path/dirname path)))) + :lpy (let [root (os.path/dirname path)] + (when-not (= path root) root)))) (defn basename [path] #?(:clj (.getName (io/file path)) From d37365406e4b3a3ed83172527e6bf198b60aa9cc Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 17:19:08 -0700 Subject: [PATCH 11/15] Enable basilisp windows CI --- .github/workflows/clojure.yml | 2 +- dev/tasks/test.clj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/clojure.yml b/.github/workflows/clojure.yml index c0479b16..2d7c8199 100644 --- a/.github/workflows/clojure.yml +++ b/.github/workflows/clojure.yml @@ -106,7 +106,7 @@ jobs: needs: [ setup, build ] strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: browser-actions/setup-chrome@v1 diff --git a/dev/tasks/test.clj b/dev/tasks/test.clj index 75301cda..6b8313f3 100644 --- a/dev/tasks/test.clj +++ b/dev/tasks/test.clj @@ -63,7 +63,7 @@ (defn lpy [] (py/install) - (t/lpy :run "-n" :portal.test-runner)) + (t/lpy :run "--include-path" "test" "--include-path" "src" "-n" :portal.test-runner)) (defn test* [] (future (cljs-runtime "1.10.773")) From f00e4f4f84d285662de8d12223108ef45f5e46d3 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 17:34:38 -0700 Subject: [PATCH 12/15] Implement `:app false` for basilisp --- src/portal/runtime/browser.cljc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/portal/runtime/browser.cljc b/src/portal/runtime/browser.cljc index d259a1b9..3092f232 100644 --- a/src/portal/runtime/browser.cljc +++ b/src/portal/runtime/browser.cljc @@ -25,7 +25,9 @@ [portal.runtime.fs :as fs] [portal.runtime.json :as json] [portal.runtime.shell :as shell])) - #?(:cljr (:import [System.Runtime.InteropServices OSPlatform RuntimeInformation]))) + #?(:cljr (:import [System.Runtime.InteropServices OSPlatform RuntimeInformation]) + :lpy (:import [os :as os] + [webbrowser :as browser]))) (defmulti -open (comp :launcher :options)) @@ -133,7 +135,8 @@ (defn- get-browser [] #?(:clj (System/getenv "BROWSER") :cljs (.-BROWSER js/process.env) - :cljr (Environment/GetEnvironmentVariable "BROWSER"))) + :cljr (Environment/GetEnvironmentVariable "BROWSER") + :lpy (.get os/environ "BROWSER"))) (defn- browse [url] (or @@ -155,7 +158,9 @@ PlatformID/Unix (if (RuntimeInformation/IsOSPlatform OSPlatform/OSX) (shell/sh "open" url) (shell/sh "xdg-open" url)) - (println "Goto" url "to view portal ui."))))) + (println "Goto" url "to view portal ui.")) + :lpy + (browser/open url)))) #?(:clj (defn- random-uuid [] (java.util.UUID/randomUUID))) From 9a8e91dc899aafa1964f35be2585cd5d012da377 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 17:52:32 -0700 Subject: [PATCH 13/15] Fix ipv6 default for basilisp --- src/portal/runtime/python/launcher.lpy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/portal/runtime/python/launcher.lpy b/src/portal/runtime/python/launcher.lpy index 590b386f..a452e2fe 100644 --- a/src/portal/runtime/python/launcher.lpy +++ b/src/portal/runtime/python/launcher.lpy @@ -65,10 +65,11 @@ (try (let [runner (web/ServerRunner (web/Server #(handler %)))] (await (.setup runner)) - (let [site (web/TCPSite runner (:host options "localhost") (:port options 0)) + (let [host (:host options "localhost") + site (web/TCPSite runner host (:port options 0)) _ (await (.start site)) event (asyncio/Event) - [host port] (.getsockname (aget (.. site -_server -sockets) 0))] + [_ port] (.getsockname (aget (.. site -_server -sockets) 0))] (try (swap! server merge {:port port From ddd8d49d4b7b698dfe9a405337285f7a20f33ef8 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 18:12:04 -0700 Subject: [PATCH 14/15] Cleanup --- src/portal/runtime/cson.cljc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/portal/runtime/cson.cljc b/src/portal/runtime/cson.cljc index eda9f511..31bb44fa 100644 --- a/src/portal/runtime/cson.cljc +++ b/src/portal/runtime/cson.cljc @@ -933,8 +933,6 @@ (let [handler (:default-handler *options* tagged-value)] (handler op (->value buffer)))))))) -#?(:lpy (def sorted? (constantly false))) - (defn write ([value] (write value nil)) ([value options] From 4a1eb53ff3a85c27871027340fc04a6f8c5bc573 Mon Sep 17 00:00:00 2001 From: Chris Badahdah Date: Sat, 2 Aug 2025 18:31:40 -0700 Subject: [PATCH 15/15] Fix inspector for partially parse-able values --- src/portal/ui/inspector.cljs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/portal/ui/inspector.cljs b/src/portal/ui/inspector.cljs index fe716732..d6855ad9 100644 --- a/src/portal/ui/inspector.cljs +++ b/src/portal/ui/inspector.cljs @@ -926,8 +926,11 @@ (defn- inspect-object* [string] (let [context (use-context)] (try - (let [v (edn/read-string string)] + (let [[v & r] (edn/read-string (str "[" string "]"))] (cond + (some? r) + [s/div string] + (nil? v) [highlight-words "nil"] (= inspect-object