Skip to content

Commit

Permalink
Merge pull request #4 from ikappaki/feature/addon
Browse files Browse the repository at this point in the history
Introduce nREPL control panel add on
  • Loading branch information
ikappaki authored Nov 9, 2024
2 parents 67f63d3 + 21cc86d commit 0486fa1
Show file tree
Hide file tree
Showing 10 changed files with 135 additions and 68 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 12 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<version>.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 `<blender-version>/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
Expand Down Expand Up @@ -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`.
Binary file added examples/blender-nrepl-addon-install.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "basilisp-blender"
version = "0.2.0"
version = "0.3.0"
description = ""
authors = ["ikappaki"]
readme = "README.md"
Expand Down
25 changes: 25 additions & 0 deletions scripts/bb_addon_create.py
Original file line number Diff line number Diff line change
@@ -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}")
13 changes: 10 additions & 3 deletions src/basilisp_blender/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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`.
Expand All @@ -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!")]


50 changes: 30 additions & 20 deletions src/basilisp_blender/control_panel.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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))
;;
)
24 changes: 24 additions & 0 deletions src/dev/nrepl_panel_addon.py
Original file line number Diff line number Diff line change
@@ -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")
42 changes: 26 additions & 16 deletions tests/basilisp_blender/integration/control_panel_test.lpy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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))

0 comments on commit 0486fa1

Please sign in to comment.