Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a4b05ba
add new pystub proxy support
DevTGHa Jan 6, 2026
c022a39
Update README.md
DevTGHa Jan 13, 2026
4ea618a
Merge pull request #5 from disguise-one/update_api_naming
DevTGHa Feb 20, 2026
59c4e26
add register module in lazy manner
DevTGHa Feb 20, 2026
d4bc444
update changelog and readme
DevTGHa Feb 20, 2026
bb87780
update ci for further test
DevTGHa Feb 20, 2026
128ac16
ruff
DevTGHa Feb 20, 2026
3c8c0ac
add test for session
DevTGHa Feb 20, 2026
2b03a13
Merge pull request #6 from disguise-one/add-lazy-module-register
DevTGHa Feb 25, 2026
927b12c
update when same function is registered
DevTGHa Mar 18, 2026
98eb47a
add publish to test pypi action
DevTGHa Mar 18, 2026
fa18ed3
add test and update changelog
DevTGHa Mar 18, 2026
c414c62
update docstring
DevTGHa Mar 18, 2026
e1f62ea
reflect PR feedback
DevTGHa Mar 20, 2026
e54d1d0
Merge pull request #7 from disguise-one/jupyter-support
DevTGHa Mar 20, 2026
0afa021
auto discover the packages to import
DevTGHa Mar 18, 2026
567ba65
remove deprecated function
DevTGHa Mar 18, 2026
03c5626
add supported builtin modules with integration test
DevTGHa Mar 19, 2026
54d08f6
fix mypy
DevTGHa Mar 19, 2026
eff35d9
reflect PR feedback
DevTGHa Mar 20, 2026
8327bc3
reflect PR feedback
DevTGHa Mar 20, 2026
8781d1e
add limit on number of modules
DevTGHa Mar 31, 2026
eadd76b
Merge pull request #8 from disguise-one/python-plugin-import
DevTGHa Mar 31, 2026
53ffa96
add release gh action
DevTGHa Mar 31, 2026
370f1e4
check ci before publish
DevTGHa Mar 31, 2026
d7ed34e
update main maintainter
DevTGHa Mar 31, 2026
a6f666c
Merge pull request #9 from disguise-one/add-release-action
DevTGHa Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ name: CI

on:
push:
branches: [main]
branches: [main, dev]
pull_request:
branches: [main]
branches: [main, dev]
workflow_call:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11"]
python-version: ["3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

Expand Down
61 changes: 61 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Publish to PyPI

on:
workflow_dispatch:

permissions:
contents: write
id-token: write

jobs:
ci:
uses: ./.github/workflows/ci.yml

publish:
needs: ci
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: pypi
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Install uv
uses: astral-sh/setup-uv@v8.0.0
with:
python-version: "3.12"

- name: Get version
id: version
run: |
VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Check version not already published
run: |
VERSION="${{ steps.version.outputs.version }}"
if uv pip index versions designer-plugin 2>/dev/null | grep -q "$VERSION"; then
echo "Version $VERSION already exists on PyPI. Aborting."
exit 1
fi

- name: Validate tag does not exist
run: |
if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
echo "Tag v${{ steps.version.outputs.version }} already exists. Aborting."
exit 1
fi

- name: Build package
run: uv build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

- name: Tag release
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}"
git push origin "v${{ steps.version.outputs.version }}"
64 changes: 64 additions & 0 deletions .github/workflows/test-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Publish to Test PyPI

on:
workflow_dispatch:

permissions:
contents: read
id-token: write # Required for trusted publishing

jobs:
test-publish:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12' # tomllib requires >= 3.11

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Append .dev suffix for unique Test PyPI versions
run: |
python -c "
import tomllib, pathlib, re
path = pathlib.Path('pyproject.toml')
text = path.read_text()
data = tomllib.loads(text)
version = data['project']['version']
dev_version = f'{version}.dev${{ github.run_number }}'
# Only replace the version inside the [project] section to avoid
# accidentally matching a version key in [tool.*] sections.
def replace_in_project_section(text, old_ver, new_ver):
project_match = re.search(r'^\[project\]', text, re.MULTILINE)
if not project_match:
raise RuntimeError('[project] section not found in pyproject.toml')
start = project_match.start()
# Find the next top-level section header or end of file
next_section = re.search(r'^\[(?!project[.\]])', text[start+1:], re.MULTILINE)
end = (start + 1 + next_section.start()) if next_section else len(text)
section = text[start:end]
section = re.sub(
r'(version\s*=\s*\")' + re.escape(old_ver) + r'\"',
r'\g<1>' + new_ver + '\"',
section, count=1,
)
return text[:start] + section + text[end:]
text = replace_in_project_section(text, version, dev_version)
path.write_text(text)
print(f'Version set to {dev_version}')
"

- name: Build package
run: uv build

- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - 2026-01-06

### Added
- **Lazy module registration**: `D3Session.execute()` and `D3AsyncSession.execute()` now automatically register a `@d3function` module on first use, eliminating the need to declare all modules in `context_modules` upfront.
- `registered_modules` tracking on session instances prevents duplicate registration calls.
- **Jupyter notebook support**: `@d3function` now automatically replaces a previously registered function when the same name is re-registered in the same module, with a warning log. This enables iterative workflows in Jupyter notebooks where cells are re-executed.
- **Automatic import detection**: `@d3function` now automatically discovers file-level imports used by the decorated function and includes them in the registered module. In Jupyter notebooks, place imports inside the function body instead.

### Removed
- `add_packages_in_current_file()`: Removed. Imports are now detected automatically by `@d3function`.
- `find_packages_in_current_file()`: Removed. Replaced by `find_imports_for_function()`.

### Changed
- `d3_api_plugin` has been renamed to `d3_api_execute`.
- `d3_api_aplugin` has been renamed to `d3_api_aexecute`.
- `context_modules` parameter type updated from `list[str]` to `set[str]` on `D3Session`, `D3AsyncSession`, and `D3SessionBase`.
- Updated documentation to reflect `pystub` proxy support.
- Bumped `actions/checkout` to v6 and `astral-sh/setup-uv` to v7 in CI.
- Added Test PyPI publish workflow (`test-publish.yml`) for dev version releases.

## [1.2.0] - 2025-12-02

### Added
Expand Down
11 changes: 8 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,19 @@ Thank you for your interest in contributing to designer-plugin! This document pr

### Running Tests

Run the full test suite:
Run unit tests (default):
```bash
uv run pytest
```

Run tests with verbose output:
Run integration tests (requires a running d3 instance):
```bash
uv run pytest -v
uv run pytest -m integration
```

Run all tests:
```bash
uv run pytest -m ""
```

Run specific test file:
Expand Down
33 changes: 18 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ To enable IDE autocomplete and type checking for Designer's Python API, install
pip install designer-plugin-pystub
```

Once installed, import the stubs using the `TYPE_CHECKING` pattern. This provides type hints in your IDE without affecting runtime execution:
Once installed, import the stubs.
> **Important:** `pystub` provides type hints for Designer's API objects but not their implementations. These objects only exist in Designer's runtime and cannot be used in local Python code. They must only be referenced in code that will be executed remotely in Designer.

```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from designer_plugin.pystub.d3 import *
from designer_plugin.pystub import *
```

This allows you to get autocomplete for Designer objects like `resourceManager`, `Screen2`, `Path`, etc., while writing your plugin code.
Expand All @@ -100,9 +100,7 @@ The Client API allows you to define a class with methods that execute remotely o

```python
from designer_plugin.d3sdk import D3PluginClient
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from designer_plugin.pystub.d3 import *
from designer_plugin.pystub import *

# 1. Sync example -----------------------------------
class MySyncPlugin(D3PluginClient):
Expand Down Expand Up @@ -169,7 +167,15 @@ The Functional API offers two decorators: `@d3pythonscript` and `@d3function`:
- **`@d3function`**:
- Must be registered on Designer before execution.
- Functions decorated with the same `module_name` are grouped together and can call each other, enabling function chaining and code reuse.
- Registration is automatic when you pass module names to the session context manager (e.g., `D3AsyncSession('localhost', 80, ["mymodule"])`). If you don't provide module names, no registration occurs.
- Registration happens automatically on the first call to `execute()` or `rpc()` that references the module — no need to declare modules upfront. You can also pre-register specific modules by passing them to the session context manager (e.g., `D3AsyncSession('localhost', 80, {"mymodule"})`).

> **Jupyter Notebook:** File-level imports (e.g., `import numpy as np` in a separate cell) cannot be automatically detected. In Jupyter, place any required imports inside the function body itself:
> ```python
> @d3function("mymodule")
> def my_fn():
> import numpy as np
> return np.array([1, 2])
> ```

### Session API Methods

Expand All @@ -186,9 +192,7 @@ Both `D3AsyncSession` and `D3Session` provide two methods for executing function

```python
from designer_plugin.d3sdk import d3pythonscript, d3function, D3AsyncSession
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from designer_plugin.pystub.d3 import *
from designer_plugin.pystub import *

# 1. @d3pythonscript - simple one-off execution
@d3pythonscript
Expand All @@ -213,11 +217,11 @@ def my_time() -> str:
return str(datetime.datetime.now())

# Usage with async session
async with D3AsyncSession('localhost', 80, ["mymodule"]) as session:
async with D3AsyncSession('localhost', 80) as session:
# d3pythonscript: no registration needed
await session.rpc(rename_surface.payload("surface 1", "surface 2"))

# d3function: registered automatically via context manager
# d3function: module is registered automatically on first call
time: str = await session.rpc(
rename_surface_get_time.payload("surface 1", "surface 2"))

Expand All @@ -230,7 +234,7 @@ async with D3AsyncSession('localhost', 80, ["mymodule"]) as session:

# Sync usage
from designer_plugin.d3sdk import D3Session
with D3Session('localhost', 80, ["mymodule"]) as session:
with D3Session('localhost', 80) as session:
session.rpc(rename_surface.payload("surface 1", "surface 2"))
```

Expand All @@ -251,4 +255,3 @@ logging.getLogger('designer_plugin').setLevel(logging.DEBUG)
# License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"

[project]
name = "designer-plugin"
version = "1.2.1"
version = "1.3.0"
description = "Python library for creating Disguise Designer plugins with DNS-SD discovery and remote Python execution"
authors = [
{ name = "Tom Whittock", email = "tom.whittock@disguise.one" },
{ name = "Taegyun Ha", email = "taegyun.ha@disguise.one" }
{ name = "Taegyun Ha", email = "taegyun.ha@disguise.one" },
{ name = "Tom Whittock", email = "tom.whittock@disguise.one" }
]
dependencies = [
"aiohttp>=3.13.2",
Expand Down Expand Up @@ -109,6 +109,11 @@ python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"-m", "not integration",
"--strict-markers",
"--strict-config",
]
markers = [
"integration: tests that require a running d3 instance",
]

4 changes: 2 additions & 2 deletions src/designer_plugin/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ async def d3_api_arequest(

###############################################################################
# API async interface
async def d3_api_aplugin(
async def d3_api_aexecute(
hostname: str,
port: int,
payload: PluginPayload[RetType],
Expand Down Expand Up @@ -219,7 +219,7 @@ async def d3_api_aregister_module(

###############################################################################
# API sync interface
def d3_api_plugin(
def d3_api_execute(
hostname: str,
port: int,
payload: PluginPayload[RetType],
Expand Down
4 changes: 2 additions & 2 deletions src/designer_plugin/d3sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from .client import D3PluginClient
from .function import (
add_packages_in_current_file,
PackageInfo,
d3function,
d3pythonscript,
get_all_d3functions,
Expand All @@ -18,9 +18,9 @@
"D3AsyncSession",
"D3PluginClient",
"D3Session",
"PackageInfo",
"d3pythonscript",
"d3function",
"add_packages_in_current_file",
"get_register_payload",
"get_all_d3functions",
"get_all_modules",
Expand Down
Loading
Loading