Skip to content

Commit 15446fc

Browse files
authored
Merge pull request #10 from disguise-one/dev
Merge dev into main
2 parents 3f33f62 + a6f666c commit 15446fc

20 files changed

Lines changed: 1196 additions & 211 deletions

.github/workflows/ci.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ name: CI
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, dev]
66
pull_request:
7-
branches: [main]
7+
branches: [main, dev]
8+
workflow_call:
89

910
jobs:
1011
test:
1112
runs-on: ubuntu-latest
1213
strategy:
14+
fail-fast: false
1315
matrix:
14-
python-version: ["3.11"]
16+
python-version: ["3.11", "3.12", "3.13"]
1517

1618
steps:
17-
- uses: actions/checkout@v4
19+
- uses: actions/checkout@v6
1820

1921
- name: Install uv
20-
uses: astral-sh/setup-uv@v5
22+
uses: astral-sh/setup-uv@v7
2123
with:
2224
enable-cache: true
2325

.github/workflows/release.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: write
8+
id-token: write
9+
10+
jobs:
11+
ci:
12+
uses: ./.github/workflows/ci.yml
13+
14+
publish:
15+
needs: ci
16+
runs-on: ubuntu-latest
17+
if: github.ref == 'refs/heads/main'
18+
environment: pypi
19+
steps:
20+
- uses: actions/checkout@v6
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Install uv
25+
uses: astral-sh/setup-uv@v8.0.0
26+
with:
27+
python-version: "3.12"
28+
29+
- name: Get version
30+
id: version
31+
run: |
32+
VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
33+
echo "version=$VERSION" >> $GITHUB_OUTPUT
34+
35+
- name: Check version not already published
36+
run: |
37+
VERSION="${{ steps.version.outputs.version }}"
38+
if uv pip index versions designer-plugin 2>/dev/null | grep -q "$VERSION"; then
39+
echo "Version $VERSION already exists on PyPI. Aborting."
40+
exit 1
41+
fi
42+
43+
- name: Validate tag does not exist
44+
run: |
45+
if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
46+
echo "Tag v${{ steps.version.outputs.version }} already exists. Aborting."
47+
exit 1
48+
fi
49+
50+
- name: Build package
51+
run: uv build
52+
53+
- name: Publish to PyPI
54+
uses: pypa/gh-action-pypi-publish@release/v1
55+
56+
- name: Tag release
57+
run: |
58+
git config user.name "github-actions[bot]"
59+
git config user.email "github-actions[bot]@users.noreply.github.com"
60+
git tag "v${{ steps.version.outputs.version }}"
61+
git push origin "v${{ steps.version.outputs.version }}"

.github/workflows/test-publish.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Publish to Test PyPI
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: read
8+
id-token: write # Required for trusted publishing
9+
10+
jobs:
11+
test-publish:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v6
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v6
20+
with:
21+
python-version: '3.12' # tomllib requires >= 3.11
22+
23+
- name: Install uv
24+
uses: astral-sh/setup-uv@v7
25+
26+
- name: Append .dev suffix for unique Test PyPI versions
27+
run: |
28+
python -c "
29+
import tomllib, pathlib, re
30+
path = pathlib.Path('pyproject.toml')
31+
text = path.read_text()
32+
data = tomllib.loads(text)
33+
version = data['project']['version']
34+
dev_version = f'{version}.dev${{ github.run_number }}'
35+
# Only replace the version inside the [project] section to avoid
36+
# accidentally matching a version key in [tool.*] sections.
37+
def replace_in_project_section(text, old_ver, new_ver):
38+
project_match = re.search(r'^\[project\]', text, re.MULTILINE)
39+
if not project_match:
40+
raise RuntimeError('[project] section not found in pyproject.toml')
41+
start = project_match.start()
42+
# Find the next top-level section header or end of file
43+
next_section = re.search(r'^\[(?!project[.\]])', text[start+1:], re.MULTILINE)
44+
end = (start + 1 + next_section.start()) if next_section else len(text)
45+
section = text[start:end]
46+
section = re.sub(
47+
r'(version\s*=\s*\")' + re.escape(old_ver) + r'\"',
48+
r'\g<1>' + new_ver + '\"',
49+
section, count=1,
50+
)
51+
return text[:start] + section + text[end:]
52+
text = replace_in_project_section(text, version, dev_version)
53+
path.write_text(text)
54+
print(f'Version set to {dev_version}')
55+
"
56+
57+
- name: Build package
58+
run: uv build
59+
60+
- name: Publish to Test PyPI
61+
uses: pypa/gh-action-pypi-publish@release/v1
62+
with:
63+
repository-url: https://test.pypi.org/legacy/
64+
skip-existing: true

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.3.0] - 2026-01-06
9+
10+
### Added
11+
- **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.
12+
- `registered_modules` tracking on session instances prevents duplicate registration calls.
13+
- **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.
14+
- **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.
15+
16+
### Removed
17+
- `add_packages_in_current_file()`: Removed. Imports are now detected automatically by `@d3function`.
18+
- `find_packages_in_current_file()`: Removed. Replaced by `find_imports_for_function()`.
19+
20+
### Changed
21+
- `d3_api_plugin` has been renamed to `d3_api_execute`.
22+
- `d3_api_aplugin` has been renamed to `d3_api_aexecute`.
23+
- `context_modules` parameter type updated from `list[str]` to `set[str]` on `D3Session`, `D3AsyncSession`, and `D3SessionBase`.
24+
- Updated documentation to reflect `pystub` proxy support.
25+
- Bumped `actions/checkout` to v6 and `astral-sh/setup-uv` to v7 in CI.
26+
- Added Test PyPI publish workflow (`test-publish.yml`) for dev version releases.
27+
828
## [1.2.0] - 2025-12-02
929

1030
### Added

CONTRIBUTING.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,19 @@ Thank you for your interest in contributing to designer-plugin! This document pr
3131

3232
### Running Tests
3333

34-
Run the full test suite:
34+
Run unit tests (default):
3535
```bash
3636
uv run pytest
3737
```
3838

39-
Run tests with verbose output:
39+
Run integration tests (requires a running d3 instance):
4040
```bash
41-
uv run pytest -v
41+
uv run pytest -m integration
42+
```
43+
44+
Run all tests:
45+
```bash
46+
uv run pytest -m ""
4247
```
4348

4449
Run specific test file:

README.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ To enable IDE autocomplete and type checking for Designer's Python API, install
8383
pip install designer-plugin-pystub
8484
```
8585

86-
Once installed, import the stubs using the `TYPE_CHECKING` pattern. This provides type hints in your IDE without affecting runtime execution:
86+
Once installed, import the stubs.
87+
> **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.
88+
8789
```python
88-
from typing import TYPE_CHECKING
89-
if TYPE_CHECKING:
90-
from designer_plugin.pystub.d3 import *
90+
from designer_plugin.pystub import *
9191
```
9292

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

101101
```python
102102
from designer_plugin.d3sdk import D3PluginClient
103-
from typing import TYPE_CHECKING
104-
if TYPE_CHECKING:
105-
from designer_plugin.pystub.d3 import *
103+
from designer_plugin.pystub import *
106104

107105
# 1. Sync example -----------------------------------
108106
class MySyncPlugin(D3PluginClient):
@@ -169,7 +167,15 @@ The Functional API offers two decorators: `@d3pythonscript` and `@d3function`:
169167
- **`@d3function`**:
170168
- Must be registered on Designer before execution.
171169
- Functions decorated with the same `module_name` are grouped together and can call each other, enabling function chaining and code reuse.
172-
- 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.
170+
- 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"})`).
171+
172+
> **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:
173+
> ```python
174+
> @d3function("mymodule")
175+
> def my_fn():
176+
> import numpy as np
177+
> return np.array([1, 2])
178+
> ```
173179
174180
### Session API Methods
175181
@@ -186,9 +192,7 @@ Both `D3AsyncSession` and `D3Session` provide two methods for executing function
186192
187193
```python
188194
from designer_plugin.d3sdk import d3pythonscript, d3function, D3AsyncSession
189-
from typing import TYPE_CHECKING
190-
if TYPE_CHECKING:
191-
from designer_plugin.pystub.d3 import *
195+
from designer_plugin.pystub import *
192196
193197
# 1. @d3pythonscript - simple one-off execution
194198
@d3pythonscript
@@ -213,11 +217,11 @@ def my_time() -> str:
213217
return str(datetime.datetime.now())
214218
215219
# Usage with async session
216-
async with D3AsyncSession('localhost', 80, ["mymodule"]) as session:
220+
async with D3AsyncSession('localhost', 80) as session:
217221
# d3pythonscript: no registration needed
218222
await session.rpc(rename_surface.payload("surface 1", "surface 2"))
219223
220-
# d3function: registered automatically via context manager
224+
# d3function: module is registered automatically on first call
221225
time: str = await session.rpc(
222226
rename_surface_get_time.payload("surface 1", "surface 2"))
223227
@@ -230,7 +234,7 @@ async with D3AsyncSession('localhost', 80, ["mymodule"]) as session:
230234
231235
# Sync usage
232236
from designer_plugin.d3sdk import D3Session
233-
with D3Session('localhost', 80, ["mymodule"]) as session:
237+
with D3Session('localhost', 80) as session:
234238
session.rpc(rename_surface.payload("surface 1", "surface 2"))
235239
```
236240
@@ -251,4 +255,3 @@ logging.getLogger('designer_plugin').setLevel(logging.DEBUG)
251255
# License
252256
253257
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
254-

pyproject.toml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "designer-plugin"
7-
version = "1.2.1"
7+
version = "1.3.0"
88
description = "Python library for creating Disguise Designer plugins with DNS-SD discovery and remote Python execution"
99
authors = [
10-
{ name = "Tom Whittock", email = "tom.whittock@disguise.one" },
11-
{ name = "Taegyun Ha", email = "taegyun.ha@disguise.one" }
10+
{ name = "Taegyun Ha", email = "taegyun.ha@disguise.one" },
11+
{ name = "Tom Whittock", email = "tom.whittock@disguise.one" }
1212
]
1313
dependencies = [
1414
"aiohttp>=3.13.2",
@@ -109,6 +109,11 @@ python_classes = ["Test*"]
109109
python_functions = ["test_*"]
110110
addopts = [
111111
"-v",
112+
"-m", "not integration",
112113
"--strict-markers",
113114
"--strict-config",
114115
]
116+
markers = [
117+
"integration: tests that require a running d3 instance",
118+
]
119+

src/designer_plugin/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async def d3_api_arequest(
125125

126126
###############################################################################
127127
# API async interface
128-
async def d3_api_aplugin(
128+
async def d3_api_aexecute(
129129
hostname: str,
130130
port: int,
131131
payload: PluginPayload[RetType],
@@ -219,7 +219,7 @@ async def d3_api_aregister_module(
219219

220220
###############################################################################
221221
# API sync interface
222-
def d3_api_plugin(
222+
def d3_api_execute(
223223
hostname: str,
224224
port: int,
225225
payload: PluginPayload[RetType],

src/designer_plugin/d3sdk/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from .client import D3PluginClient
77
from .function import (
8-
add_packages_in_current_file,
8+
PackageInfo,
99
d3function,
1010
d3pythonscript,
1111
get_all_d3functions,
@@ -18,9 +18,9 @@
1818
"D3AsyncSession",
1919
"D3PluginClient",
2020
"D3Session",
21+
"PackageInfo",
2122
"d3pythonscript",
2223
"d3function",
23-
"add_packages_in_current_file",
2424
"get_register_payload",
2525
"get_all_d3functions",
2626
"get_all_modules",

0 commit comments

Comments
 (0)