From 927b12c21c9902631d77a92926f691550bbb3e5d Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 18 Mar 2026 12:13:59 +0000 Subject: [PATCH 1/4] update when same function is registered when user use jupyter notbook, it is exected to register same function with different definition multiple times. update the function to reflect the change and give an warning. --- src/designer_plugin/d3sdk/function.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/designer_plugin/d3sdk/function.py b/src/designer_plugin/d3sdk/function.py index c417755..bce5cd5 100644 --- a/src/designer_plugin/d3sdk/function.py +++ b/src/designer_plugin/d3sdk/function.py @@ -6,6 +6,7 @@ import ast import functools import inspect +import logging import textwrap from collections import defaultdict from collections.abc import Callable @@ -24,6 +25,8 @@ RegisterPayload, ) +logger = logging.getLogger(__name__) + ############################################################################### # Plugin function related implementations @@ -253,6 +256,16 @@ def __init__(self, module_name: str, func: Callable[P, T]): super().__init__(func) + # Update the function in case the function was updated in same session. + # For example, jupyter notebook server can be running, but function signature can + # change constantly. + if self in D3Function._available_d3functions[module_name]: + logger.warning( + "Function '%s' in module '%s' is being replaced.", + self.name, + module_name, + ) + D3Function._available_d3functions[module_name].discard(self) D3Function._available_d3functions[module_name].add(self) def __eq__(self, other: object) -> bool: From 98eb47a13f3a678bcb68200cfdb8ea58d798f496 Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 18 Mar 2026 12:14:29 +0000 Subject: [PATCH 2/4] add publish to test pypi action --- .github/workflows/ci.yml | 4 +- .github/workflows/test-publish.yml | 63 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test-publish.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6963110..e4d614c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,10 @@ jobs: 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 diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml new file mode 100644 index 0000000..7075612 --- /dev/null +++ b/.github/workflows/test-publish.yml @@ -0,0 +1,63 @@ +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/ From fa18ed3180678f6761ba6e8487143babb0b295bc Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 18 Mar 2026 12:20:23 +0000 Subject: [PATCH 3/4] add test and update changelog --- CHANGELOG.md | 3 +++ tests/test_core.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4128fb..88955e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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. ### 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 diff --git a/tests/test_core.py b/tests/test_core.py index e71ea9d..d100f18 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,6 +3,8 @@ Copyright (c) 2025 Disguise Technologies ltd """ +import logging + import pytest from designer_plugin.d3sdk.function import ( @@ -329,6 +331,61 @@ def test_inequality_different_functions(self): +class TestD3FunctionReplacement: + """Test that re-registering a D3Function with the same name replaces the old one.""" + + def test_reregister_replaces_function(self): + """Re-registering a function with the same name should replace it in the set.""" + module = "test_replace_module" + D3Function._available_d3functions[module].clear() + + @d3function(module) + def my_func(a: int) -> int: + return a + + @d3function(module) + def my_func(a: int, b: int) -> int: # noqa: F811 + return a + b + + funcs = D3Function._available_d3functions[module] + matching = [f for f in funcs if f.name == "my_func"] + assert len(matching) == 1 + assert matching[0].function_info.args == ["a", "b"] + + def test_reregister_logs_warning(self, caplog): + """Re-registering should log a warning.""" + module = "test_replace_warn_module" + D3Function._available_d3functions[module].clear() + + @d3function(module) + def warn_func() -> None: + pass + + with caplog.at_level(logging.WARNING, logger="designer_plugin.d3sdk.function"): + @d3function(module) + def warn_func() -> int: # noqa: F811 + return 1 + + assert any("warn_func" in msg and "being replaced" in msg for msg in caplog.messages) + + def test_set_size_unchanged_after_replacement(self): + """The function set size should stay the same after replacement.""" + module = "test_replace_size_module" + D3Function._available_d3functions[module].clear() + + @d3function(module) + def size_func(x: int) -> int: + return x + + assert len(D3Function._available_d3functions[module]) == 1 + + @d3function(module) + def size_func(x: int, y: int) -> int: # noqa: F811 + return x + y + + assert len(D3Function._available_d3functions[module]) == 1 + + class TestD3PythonScript: def test_d3pythonscript_decorator(self): @d3pythonscript From c414c624b788bc7365450f716f13b5d8793b33ca Mon Sep 17 00:00:00 2001 From: Taegyun Ha Date: Wed, 18 Mar 2026 12:28:38 +0000 Subject: [PATCH 4/4] update docstring --- src/designer_plugin/d3sdk/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/designer_plugin/d3sdk/session.py b/src/designer_plugin/d3sdk/session.py index c32b820..154e4c3 100644 --- a/src/designer_plugin/d3sdk/session.py +++ b/src/designer_plugin/d3sdk/session.py @@ -61,7 +61,7 @@ def __init__( Args: hostname: The hostname of the Designer instance. port: The port number of the Designer instance. - context_modules: Optional list of module names to register when entering session context. + context_modules: Optional set of module names to register when entering session context. """ super().__init__(hostname, port, context_modules or set()) @@ -198,7 +198,7 @@ def __init__( Args: hostname: The hostname of the Designer instance. port: The port number of the Designer instance. - context_modules: Optional list of module names to register when entering session context. + context_modules: Optional set of module names to register when entering session context. """ super().__init__(hostname, port, context_modules or set())