├── .gitignore ├── README.org ├── deps.edn ├── resources ├── clj.py └── requirements.txt └── src └── clj_python_trampoline ├── interpreter.clj └── resources.clj /.gitignore: -------------------------------------------------------------------------------- 1 | classes 2 | __pycache__ 3 | .cpcache -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Motivation 2 | 3 | I want to use libpython-clj from an already running python process, without needing any special python builds. 4 | 5 | * Examples 6 | 7 | [[https://github.com/tristanstraub/blender-clj-addon][blender-clj-addon]] 8 | 9 | * Running python with embedded nREPL 10 | 11 | The following prepares and runs the python process (or application with embedded python) and bootstraps nREPL via a python script, passed via the command line. 12 | 13 | 1. Preparing python environment: 14 | 15 | #+BEGIN_SRC sh 16 | clj -m clj-python-trampoline.resources --requirements > requirements.txt 17 | pip3 install -r requirements.txt 18 | 19 | clj -m clj-python-trampoline.resources --clj > clj.py 20 | #+END_SRC 21 | 22 | 2. Starting a REPL: 23 | 24 | i. from the command line: 25 | 26 | #+BEGIN_SRC sh 27 | python3 clj.py 28 | #+END_SRC 29 | 30 | ii. with nREPL: 31 | 32 | #+BEGIN_SRC sh 33 | python3 clj.py -- -e "(require 'nrepl.cmdline) (future (nrepl.cmdline/-main))" 34 | #+END_SRC 35 | 36 | iii. from *blender*, with nREPL and cider middleware: 37 | 38 | #+BEGIN_SRC sh 39 | export CLASSPATH="$(clj -Sdeps '{:deps {nrepl {:mvn/version "0.7.0"} refactor-nrepl {:mvn/version "RELEASE"} cider/cider-nrepl {:mvn/version "RELEASE"}}}' -Spath)" 40 | 41 | blender -P clj.py -- -e "(require 'nrepl.cmdline) (future (nrepl.cmdline/-main \"--middleware\" \"[\\\"refactor-nrepl.middleware/wrap-refactor\\\",\\\"cider.nrepl/cider-middleware\\\"]\"))" 42 | #+END_SRC 43 | 44 | 3. Connect to remote nREPL, and load python bindings: 45 | 46 | #+BEGIN_SRC sh 47 | clj -m nrepl.cmdline -c -p $(cat .nrepl-port) 48 | #+END_SRC 49 | 50 | #+BEGIN_SRC clojure 51 | (require '[libpython-clj.require :refer [require-python]]) ;; loads python shared library for us, calling our patched libpython-clj 52 | (require-python 'sys) 53 | (println sys/version) 54 | #+END_SRC 55 | 56 | * Assumptions 57 | 58 | *javabridge* is installed with your local "pip3" command, which should be installing to the python environment that blender uses. 59 | 60 | (Tested on Fedora 31) 61 | 62 | * Implementation 63 | 64 | - *javabridge* provides the python -> jvm connection. 65 | - *libpython-clj* is patched at runtime, to be able to use it from inside an existing python process. This patch is experimental and will certainly break something. 66 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resources"] 2 | 3 | :deps {org.clojure/clojure {:mvn/version "1.10.1"} 4 | clj-python/libpython-clj {:mvn/version "1.44"} 5 | nrepl/nrepl {:mvn/version "0.7.0"}}} 6 | -------------------------------------------------------------------------------- /resources/clj.py: -------------------------------------------------------------------------------- 1 | import sys, os, javabridge, subprocess 2 | 3 | def main(*args): 4 | jars = javabridge.JARS 5 | classpath=os.getenv("CLASSPATH", None) 6 | if not classpath: 7 | classpath = subprocess.check_output("clj -Spath", shell=True).decode('utf-8').strip() 8 | 9 | if classpath: 10 | jars = jars + [classpath] 11 | 12 | javabridge.start_vm(run_headless=True, class_path=jars) 13 | 14 | env = javabridge.get_env() 15 | stringclass = env.find_class("java/lang/String") 16 | 17 | if len(args) == 0: 18 | args = ["-r"] 19 | 20 | args = ["-e", "(require 'clj-python-trampoline.interpreter)"] + list(args) 21 | 22 | cljargs = env.make_object_array(len(args), stringclass) 23 | 24 | for i, arg in enumerate(args): 25 | env.set_object_array_element(cljargs, i, env.new_string_utf(arg)) 26 | 27 | c = env.find_class("clojure/main") 28 | method_id = env.get_static_method_id(c, "main", "([Ljava/lang/String;)V") 29 | env.call_static_method(c, method_id, cljargs) 30 | 31 | if __name__ == "__main__": 32 | 33 | args = sys.argv[:] 34 | 35 | if '--' in args: 36 | main(*args[args.index("--") + 1:]) 37 | else: 38 | main() 39 | -------------------------------------------------------------------------------- /resources/requirements.txt: -------------------------------------------------------------------------------- 1 | javabridge >= 1.0.19 2 | -------------------------------------------------------------------------------- /src/clj_python_trampoline/interpreter.clj: -------------------------------------------------------------------------------- 1 | (ns clj-python-trampoline.interpreter) 2 | 3 | (require '[libpython-clj.python.interpreter]) 4 | 5 | (defn initialize! 6 | [& args] 7 | (apply libpython-clj.python.interpreter/initialize! args)) 8 | 9 | (in-ns 'libpython-clj.python.interpreter) 10 | 11 | ;; Directly out of libpython-clj. Obviously, changes like this belong in libpython-clj, 12 | ;; but this idea is still being developed. 13 | (defn initialize! 14 | [& {:keys [program-name 15 | library-path] 16 | :as options}] 17 | (when-not (main-interpreter) 18 | (log-info (str "Executing python initialize with options:" options)) 19 | (let [{:keys [python-home libnames java-library-path-addendum 20 | executable] :as startup-info} 21 | (detect-startup-info options) 22 | library-names (concat 23 | (when library-path 24 | [library-path]) 25 | libnames 26 | (libpy-base/library-names))] 27 | (reset! python-home-wide-ptr* nil) 28 | (reset! python-path-wide-ptr* nil) 29 | (log/infof "Trying python library names %s" (vec library-names)) 30 | (when python-home 31 | (append-java-library-path! java-library-path-addendum) 32 | (reset! python-home-wide-ptr* (jna/string->wide-ptr python-home)) 33 | (reset! python-path-wide-ptr* (jna/string->wide-ptr 34 | (format "%s/bin/python3" 35 | python-home)))) 36 | (loop [[library-name & library-names] library-names] 37 | (if (and library-name 38 | (not (try-load-python-library! library-name 39 | @python-home-wide-ptr* 40 | @python-path-wide-ptr*))) 41 | (recur library-names))) 42 | (setup-direct-mapping!)) 43 | 44 | (construct-main-interpreter! nil nil))) 45 | 46 | (in-ns 'clj-python-trampoline.interpreter) 47 | -------------------------------------------------------------------------------- /src/clj_python_trampoline/resources.clj: -------------------------------------------------------------------------------- 1 | (ns clj-python-trampoline.resources 2 | (:require [clojure.java.io :as io])) 3 | 4 | (defn resource-clj 5 | [] 6 | (print (slurp (io/resource "clj.py")))) 7 | 8 | (defn resource-requirements 9 | [] 10 | (print (slurp (io/resource "requirements.txt")))) 11 | 12 | (defn -main 13 | [& args] 14 | (doseq [arg args] 15 | (cond (= arg "--clj") 16 | (resource-clj) 17 | (= arg "--requirements") 18 | (resource-requirements)))) 19 | --------------------------------------------------------------------------------