diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 14b7f80..25ab035 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -30,9 +30,9 @@ jobs: python -m pip install --upgrade pip pip install poetry - name: Build package - run: poetry build + run: poetry build && python scripts/bb_addon_create.py - name: Upload release artifacts - run: gh release upload ${{ github.event.release.tag_name }} dist/*.{tar.gz,whl} + run: gh release upload ${{ github.event.release.tag_name }} dist/*.{tar.gz,whl} dist/nrepl_panel_addon*.py env: GH_TOKEN: ${{ github.token }} - name: Publish package distributions to PyPI diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d2db7c..b1bb69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +## 0.3.0 + +- Made the nREPL control panel destructible. +- Released a Blender Add-on to display the nREPL control panel. + ## 0.2.0 - Added async server interface support in `start-server!` with a client work abstraction. diff --git a/README.md b/README.md index 6ad0bba..86d7689 100644 --- a/README.md +++ b/README.md @@ -5,51 +5,37 @@ [Basilisp](https://github.com/basilisp-lang/basilisp) is a Python-based Lisp implementation that offers broad compatibility with Clojure. For more details, refer to the [documentation](https://basilisp.readthedocs.io/en/latest/index.html). ## Overview -`basilisp-blender` is a Python library designed to facilitate the execution of Basilisp Clojure code within [Blender](https://www.blender.org/) and manage an nREPL server for interactive programming. +`basilisp-blender` is a Python library designed to facilitate the execution of Basilisp Clojure code within [Blender](https://www.blender.org/) and manage an nREPL server for interactive programming from within your editor of choice. This library provides functions to evaluate Basilisp code from Blender's Python console, file or Text Editor and to start an nREPL server, allowing seamless integration and communication with Basilisp. ## Installation -To install `basilisp-blender`, use `pip` from Blender's Python console: +To install `basilisp-blender`, use `pip install` from Python console within the Blender's `Scripting` workspace: ```python import pip pip.main(['install', 'basilisp-blender']) ``` +Adjust the command as needed for your environment. For instance, use `-U` to upgrade to the latest version or `--user` to install to your user directory. For additional options, refer to [pip options](https://pip.pypa.io/en/stable/cli/pip_install/). + ## Setup ### nREPL server control panel -The library includes an nREPL server control panel accessible in Blender’s properties editor, under the Output panel (icon resembling a printer). From here, users can: +The library includes an nREPL server control panel accessible in Blender’s Properties editor, under the Output panel (icon resembling a printer). From here, users can: - Start and stop the server. - Configure the local interface address and port. -- Set the location of the `.nrepl-port` file for editor connections. - +- Specify your Basilisp project's root directory, where the `.nrepl-port` file will be saved for editor discovery. -![nrepl cntrl pnael output - ready](examples/nrepl-ctrl-panel-output-ready.png) +![nrepl cntrl panel output - ready](examples/nrepl-ctrl-panel-output-ready.png) -![nrepl cntrl pnael output - ready](examples/nrepl-ctrl-panel-output-serving.png) +![nrepl cntrl panel output - serving](examples/nrepl-ctrl-panel-output-serving.png) -Note: The control panel does not appear automatically and must be activated manually via Blender's Python console within the `Scripting` workspace. To activate, run: +To enable the control panel, download the latest `nrepl_panel_addon_.py` file from the [releases](https://github.com/ikappaki/basilisp-blender/releases) and install via`Edit`>`Preferences`>`Add-ons`>`Install From Disk`. -```python -import basilisp_blender -basilisp_blender.control_panel_create() -``` +The add-on should appear in list--be sure to check its box to activate it. -To autoload the panel automatically at Blender’s startup, create a startup file in Blender's `/scripts/startup/` directory. For example, save the code below, say as `bb.py`, in that directory: - -```python -import basilisp_blender -basilisp_blender.control_panel_create() - -def register(): - pass -def unregister(): - pass -if __name__ == "__main__": - register() -``` +![nrepl cntrl panel addon](examples/blender-nrepl-addon-install.png) ## Usage ### Evaluating Basilisp Code @@ -242,4 +228,4 @@ This project is licensed under the Eclipse Public License 2.0. See the [LICENSE] # Acknowledgments -The nREPL server is a spin-off of [Basilisp](https://github.com/basilisp-lang/basilisp)'s `basilisp.contrib.nrepl-server` namespace. +The nREPL server is a spin-off of [Basilisp](https://github.com/basilisp-lang/basilisp)'s `basilisp.contrib.nrepl-server`. diff --git a/examples/blender-nrepl-addon-install.png b/examples/blender-nrepl-addon-install.png new file mode 100644 index 0000000..0ac83e5 Binary files /dev/null and b/examples/blender-nrepl-addon-install.png differ diff --git a/pyproject.toml b/pyproject.toml index 757e42f..6ab3527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "basilisp-blender" -version = "0.2.0" +version = "0.3.0" description = "" authors = ["ikappaki"] readme = "README.md" diff --git a/scripts/bb_addon_create.py b/scripts/bb_addon_create.py new file mode 100644 index 0000000..95187a9 --- /dev/null +++ b/scripts/bb_addon_create.py @@ -0,0 +1,25 @@ +# This script should be invoked from the project root directory. +# +# Copies the nrepl_panel_addon.py file to dist adding the version +# number as retrieved from `poetry version`. +import re +import subprocess + +src_path = "src/dev/nrepl_panel_addon.py" +version_mark = "(0, 99, 99)" + +result = subprocess.run(["poetry", "version"], capture_output=True, text=True) +_, version = result.stdout.split(" ") +major, minor, patch = version.split(".") +patch_int = int(re.match(r'^\d+', patch).group()) + +dist_path = f'dist/nrepl_panel_addon_{version.strip().replace(".", "_")}.py' + +with open(src_path, 'r') as src: + with open(dist_path, 'w', newline="\n") as dst: + dst.write(f"# Autogenerated from {src_path}\n") + for line in src.readlines(): + if version_mark in line: + line = line.replace(version_mark, f"({major}, {minor}, {patch_int})") + dst.write(line) +print(f":bb_addon_create.py :created {dist_path}") diff --git a/src/basilisp_blender/__init__.py b/src/basilisp_blender/__init__.py index 8085e07..c69c6b8 100644 --- a/src/basilisp_blender/__init__.py +++ b/src/basilisp_blender/__init__.py @@ -5,6 +5,7 @@ from basilisp import main as basilisp from basilisp.lang import compiler +from basilisp.lang.keyword import Keyword from basilisp.lang.util import munge COMPILER_OPTS = compiler.compiler_opts() @@ -13,7 +14,6 @@ LOGGER = logging.getLogger("basilisp-blender") LOGGER.addHandler(logging.StreamHandler()) - def log_level_set(level, filepath=None): """Sets the logger in the `LOGGER` global variable to the specified `level`. @@ -31,6 +31,13 @@ def log_level_set(level, filepath=None): # log_level_set(logging.DEBUG, "basilisp-blender.log") def control_panel_create(): - """Initialises and displays the nREPL server UI control panel.""" + """Initialises and displays the nREPL server UI control panel. It + returns a function to destroy the panel and settings, and stop the + server if it is running. + + """ ctrl_panel_mod = importlib.import_module(munge("basilisp-blender.control-panel")) - ctrl_panel_mod.nrepl_control_panel_create__BANG__() + panel = ctrl_panel_mod.nrepl_control_panel_create__BANG__() + return panel[Keyword("destroy!")] + + diff --git a/src/basilisp_blender/control_panel.lpy b/src/basilisp_blender/control_panel.lpy index 09b4bf6..3a43983 100644 --- a/src/basilisp_blender/control_panel.lpy +++ b/src/basilisp_blender/control_panel.lpy @@ -6,16 +6,6 @@ os.path sys)) -(defn- class-register! - "Helper function to register a bpy class, with prior unregistering if - possible." - [class] - (try - (.unregister-class bpy/utils class) - (catch Exception e - nil)) - (.register-class bpy/utils class)) - (defn- nrepl-url [host port] (str "nrepl://" host @@ -253,7 +243,14 @@ "Creates the nrepl server control panel in Blender, and returns its control interface. - The user settings are stored in the Scene as Properties." + The user settings are stored in the Scene as Properties. + + It returns a map with the following entries + + :ctrl The server control instance. + + :destroy! A function that destroys the panel and settings, and + stops the server if it is running." [] (let [ctrl (ctrl-make) settings-user @@ -265,16 +262,29 @@ panel (nrepl-control-panel-class-make ctrl)] - - (class-register! settings-user) + (bpy.utils/register-class settings-user) (set! bpy.types.Scene/nrepl-settings-user (.PointerProperty bpy/props ** :type settings-user)) - (class-register! operator) - (class-register! panel) - - ctrl)) + (bpy.utils/register-class operator) + (bpy.utils/register-class panel) + + {:ctrl ctrl + :destroy! (fn nrepl-control-panel-destroy! [] + (binding [*out* sys/stdout] + (doseq [cls [settings-user operator panel]] + (try + (bpy.utils/unregister-class cls) + (catch Exception e + nil))) + (delattr bpy.types/Scene "nrepl_settings_user") + (let [{:keys [result] :as _info} (ctrl-do! ctrl :info-get) + {:keys [status]} result] + (when (= status [:serving]) + (ctrl-do! ctrl :server-toggle!)))) + nil)})) (comment - (def ctrl2 (nrepl-control-panel-create)) - @ctrl2 -;; + (def ctrl2 (nrepl-control-panel-create!)) + @(:ctrl ctrl2) + ( (:destroy! ctrl2)) + ;; ) diff --git a/src/dev/nrepl_panel_addon.py b/src/dev/nrepl_panel_addon.py new file mode 100644 index 0000000..1219235 --- /dev/null +++ b/src/dev/nrepl_panel_addon.py @@ -0,0 +1,24 @@ +from basilisp_blender import control_panel_create +bl_info = { + "name" : "Basilisp nREPL Server Control Panel", + "description" : "Control the nREPL server from the Properties Editor>Output panel", + "author" : "ikappaki", + "version" : (0, 99, 99), + "blender" : (3, 60, 0), + "location": "Properties Editor>Output", + "doc_url" : "https://github.com/ikappaki/basilisp-blender", + "category": "Development", +} +_DESTROY_FN = None +def register(): + global _DESTROY_FN + print(f"nREPL Control Panel creating...") + _DESTROY_FN = control_panel_create() + print(f"nREPL Control Panel creating... done") + +def unregister(): + global _DESTROY_FN + print("nREPL Control Panel destroying...") + _DESTROY_FN() + _DESTROY_FN = None + print("nREPL Control Panel destroying... done") diff --git a/tests/basilisp_blender/integration/control_panel_test.lpy b/tests/basilisp_blender/integration/control_panel_test.lpy index 2938ee7..845cb50 100644 --- a/tests/basilisp_blender/integration/control_panel_test.lpy +++ b/tests/basilisp_blender/integration/control_panel_test.lpy @@ -18,19 +18,21 @@ (require '[basilisp-blender.control-panel :as p]) ;; this var will be used throughout all other tests - (def ctrl-test (p/nrepl-control-panel-create!)) + (def ctrl-panel (p/nrepl-control-panel-create!)) + (def ctrl-test (:ctrl ctrl-panel)) + (def ctrl-destroy! (:destroy! ctrl-panel)) @ctrl-test)] (is (= {:res {:status [:ready]}} result))) (is (= {:result {:host nil :status [:ready] :port nil :port-dir nil}} (:res (but/with-client-eval! - (p/ctrl-do! ctrl-test :info-get nil)))))) + (p/ctrl-do! ctrl-test :info-get)))))) (testing "server start/stop without options" (let [{:keys [res] :as ret} (but/with-client-eval! - [(p/ctrl-do! ctrl-test :server-toggle! nil) - (p/ctrl-do! ctrl-test :info-get nil)]) + [(p/ctrl-do! ctrl-test :server-toggle!) + (p/ctrl-do! ctrl-test :info-get)]) [[toggle-state toggle-msg :as toggle] info] (map :result res)] (is (= :started toggle-state)) (let [{:keys [port]} info @@ -40,8 +42,8 @@ ;; toggle -- server stop (let [{:keys [res] :as ret} (but/with-client-eval! - [(p/ctrl-do! ctrl-test :server-toggle! nil) - (p/ctrl-do! ctrl-test :info-get nil)]) + [(p/ctrl-do! ctrl-test :server-toggle!) + (p/ctrl-do! ctrl-test :info-get)]) [[toggle-state toggle-msg :as toggle] info] (map :result res)] (is (= :stopped toggle-state) ret) (is (= (str "nrepl://127.0.0.1:" port) toggle-msg) toggle) @@ -59,17 +61,17 @@ ;; toggle -- server stop (let [{:keys [res] :as ret} (but/with-client-eval! - [(p/ctrl-do! ctrl-test :server-toggle! nil) - (p/ctrl-do! ctrl-test :info-get nil)]) + [(p/ctrl-do! ctrl-test :server-toggle!) + (p/ctrl-do! ctrl-test :info-get)]) [[toggle-state toggle-msg :as toggle] info] (map :result res)] (is (= :stopped toggle-state)) (is (= (str "nrepl://0.0.0.0:" port) toggle-msg) toggle) (is (= {:host nil :status [:ready] :port nil :port-dir nil} info)))))) (testing "server port option" - (let [{:keys [res] :as ret} (but/with-client-eval! - [(p/ctrl-do! ctrl-test :server-toggle! {:port -1}) - (p/ctrl-do! ctrl-test :info-get)])] + (let [{:keys [res] :as _ret} (but/with-client-eval! + [(p/ctrl-do! ctrl-test :server-toggle! {:port -1}) + (p/ctrl-do! ctrl-test :info-get)])] (is [{:error (:server-make-error [:type-of @@ -78,25 +80,33 @@ {:result {:port nil, :host nil, :status [:ready], :port-dir nil}}] res))) (testing "server nrepl-port-dir option" - (let [{:keys [exc res] :as ret} + (let [{:keys [exc res] :as _ret} (but/with-client-eval! (import tempfile) (with [tmpdir (tempfile/TemporaryDirectory)] {:actions [(p/ctrl-do! ctrl-test :server-toggle! {:nrepl-port-dir tmpdir}) (p/ctrl-do! ctrl-test :info-get) - (p/ctrl-do! ctrl-test :server-toggle! nil)] + (p/ctrl-do! ctrl-test :server-toggle!)] :tmpdir tmpdir})) {:keys [actions tmpdir]} res [[toggle-state] {:keys [port-dir]} [toggle-state2]] - (map :result actions) + (map :result actions)] - ] (is (nil? exc) exc) (is (= :started toggle-state)) (is (= tmpdir port-dir) res) - (is (= :stopped toggle-state2)))))) + (is (= :stopped toggle-state2)))) + + (testing "destroying panel" + (let [{:keys [res]} (but/with-client-eval! + [(p/ctrl-do! ctrl-test :server-toggle!) + (ctrl-destroy!) + (p/ctrl-do! ctrl-test :info-get)]) + [[toggle-state] _ {:keys [status] :as _info}] (map :result res)] + (is (= :started toggle-state)) + (is (= [:ready] status) _info))))) #_(tu/pp-code (panel-control-test))