Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ name: CI

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

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
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [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.

### 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.

## [1.2.0] - 2025-12-02
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ 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"})`).

### Session API Methods

Expand Down Expand Up @@ -209,11 +209,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 @@ -226,7 +226,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 Down
21 changes: 15 additions & 6 deletions src/designer_plugin/d3sdk/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
class D3SessionBase:
"""Base class for Designer session management."""

def __init__(self, hostname: str, port: int, context_modules: list[str]) -> None:
def __init__(self, hostname: str, port: int, context_modules: set[str]) -> None:
"""Initialize base session with connection details and module context.

Args:
Expand All @@ -39,7 +39,8 @@ def __init__(self, hostname: str, port: int, context_modules: list[str]) -> None
"""
self.hostname: str = hostname
self.port: int = port
self.context_modules: list[str] = context_modules
self.context_modules: set[str] = context_modules
self.registered_modules: set[str] = set()
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cache registered_modules in D3SessionBase so we can register module on demand in lazy manner.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between context_modules and registered_modules?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

context_modules is modules that will get registered when entering context.
register_module is modules that is actually registered by the session.



class D3Session(D3SessionBase):
Expand All @@ -53,7 +54,7 @@ def __init__(
self,
hostname: str,
port: int = D3_PLUGIN_DEFAULT_PORT,
context_modules: list[str] | None = None,
context_modules: set[str] | None = None,
) -> None:
"""Initialize synchronous Designer session.

Expand All @@ -62,7 +63,7 @@ def __init__(
port: The port number of the Designer instance.
context_modules: Optional list of module names to register when entering session context.
"""
super().__init__(hostname, port, context_modules or [])
super().__init__(hostname, port, context_modules or set())

def __enter__(self) -> "D3Session":
"""Enter context manager and register all context modules.
Expand Down Expand Up @@ -117,6 +118,9 @@ def execute(
Raises:
PluginException: If the plugin execution fails.
"""
if payload.moduleName and payload.moduleName not in self.registered_modules:
self.register_module(payload.moduleName)

return d3_api_execute(self.hostname, self.port, payload, timeout_sec)

def request(self, method: Method, url_endpoint: str, **kwargs: Any) -> Any:
Expand Down Expand Up @@ -152,6 +156,7 @@ def register_module(
)
if payload:
d3_api_register_module(self.hostname, self.port, payload, timeout_sec)
self.registered_modules.add(module_name)
return True
return False

Expand Down Expand Up @@ -186,7 +191,7 @@ def __init__(
self,
hostname: str,
port: int = D3_PLUGIN_DEFAULT_PORT,
context_modules: list[str] | None = None,
context_modules: set[str] | None = None,
) -> None:
"""Initialize asynchronous Designer session.

Expand All @@ -195,7 +200,7 @@ def __init__(
port: The port number of the Designer instance.
context_modules: Optional list of module names to register when entering session context.
"""
super().__init__(hostname, port, context_modules or [])
super().__init__(hostname, port, context_modules or set())

async def __aenter__(self) -> "D3AsyncSession":
"""Enter async context manager and register all context modules.
Expand Down Expand Up @@ -270,6 +275,9 @@ async def execute(
Raises:
PluginException: If the plugin execution fails.
"""
if payload.moduleName and payload.moduleName not in self.registered_modules:
await self.register_module(payload.moduleName)

return await d3_api_aexecute(self.hostname, self.port, payload, timeout_sec)

async def register_module(
Expand All @@ -294,6 +302,7 @@ async def register_module(
await d3_api_aregister_module(
self.hostname, self.port, payload, timeout_sec
)
self.registered_modules.add(module_name)
return True
return False

Expand Down