From d3de220085a1bb9a676b371b7e1762b5ff7aae0a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Sat, 11 Oct 2025 11:07:21 -0400 Subject: [PATCH] Drop Union construct in python client Union is deprecated in favor of `A | B`. This updates the generation to not produce the old style and updates the old usage in __init__. --- scripts/generate-schema/gen-python.test.ts | 70 --------------- scripts/generate-schema/gen-python.ts | 56 ++++++------ .../python/src/justbe_webview/__init__.py | 14 +-- .../python/src/justbe_webview/schemas.py | 87 +++++++++---------- 4 files changed, 75 insertions(+), 152 deletions(-) delete mode 100644 scripts/generate-schema/gen-python.test.ts diff --git a/scripts/generate-schema/gen-python.test.ts b/scripts/generate-schema/gen-python.test.ts deleted file mode 100644 index 1503c92..0000000 --- a/scripts/generate-schema/gen-python.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { assertEquals } from "jsr:@std/assert"; -import dedent from "npm:dedent"; -import { extractExportedNames } from "./gen-python.ts"; - -Deno.test("extractExportedNames - extracts class names", () => { - const content = dedent` - class MyClass: - pass - - class AnotherClass(msgspec.Struct): - field: str - - def not_a_class(): - pass - `; - assertEquals(extractExportedNames(content), ["AnotherClass", "MyClass"]); -}); - -Deno.test("extractExportedNames - extracts enum assignments", () => { - const content = dedent` - MyEnum = Union[ClassA, ClassB] - AnotherEnum = Union[ClassC, ClassD, ClassE] - - not_an_enum = "something else" - `; - assertEquals(extractExportedNames(content), ["AnotherEnum", "MyEnum"]); -}); - -Deno.test("extractExportedNames - extracts both classes and enums", () => { - const content = dedent` - class MyClass: - pass - - MyEnum = Union[ClassA, ClassB] - - class AnotherClass: - field: str - - AnotherEnum = Union[ClassC, ClassD] - `; - assertEquals(extractExportedNames(content), [ - "AnotherClass", - "AnotherEnum", - "MyClass", - "MyEnum", - ]); -}); - -Deno.test("extractExportedNames - handles empty content", () => { - assertEquals(extractExportedNames(""), []); -}); - -Deno.test("extractExportedNames - ignores indented class definitions and enum assignments", () => { - const content = dedent` - def some_function(): - class IndentedClass: - pass - - IndentedEnum = Union[ClassA, ClassB] - - class TopLevelClass: - pass - - TopLevelEnum = Union[ClassC, ClassD] - `; - assertEquals(extractExportedNames(content), [ - "TopLevelClass", - "TopLevelEnum", - ]); -}); diff --git a/scripts/generate-schema/gen-python.ts b/scripts/generate-schema/gen-python.ts index f968465..aa933de 100644 --- a/scripts/generate-schema/gen-python.ts +++ b/scripts/generate-schema/gen-python.ts @@ -6,21 +6,20 @@ import { assert } from "jsr:@std/assert"; const header = (relativePath: string) => `# DO NOT EDIT: This file is auto-generated by ${relativePath}\n` + "from enum import Enum\n" + - "from typing import Any, Literal, Optional, Union\n" + "import msgspec\n\n"; export function extractExportedNames(content: string): string[] { const names = new Set(); - // Match class definitions and enum assignments + // Match class definitions and union type assignments const classRegex = /^class\s+([a-zA-Z_][a-zA-Z0-9_]*)/gm; - const enumRegex = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*Union\[/gm; + const unionRegex = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*.+\|.+/gm; let match; while ((match = classRegex.exec(content)) !== null) { names.add(match[1]); } - while ((match = enumRegex.exec(content)) !== null) { + while ((match = unionRegex.exec(content)) !== null) { names.add(match[1]); } @@ -51,10 +50,7 @@ export function generatePython( return output + content; } -function generateTypes( - doc: Doc, - name: string, -) { +function generateTypes(doc: Doc, name: string) { const writer = new Writer(); let definitions = ""; @@ -97,9 +93,7 @@ function generateTypes( return definitions + writer.output(); } -function sortByRequired( - properties: T[], -): T[] { +function sortByRequired(properties: T[]): T[] { return [...properties].sort((a, b) => { if (a.required === b.required) return 0; return a.required ? -1 : 1; @@ -116,9 +110,8 @@ function generateNode(node: Node, writer: Writer) { .with({ type: "boolean" }, () => w("bool")) .with({ type: "string" }, () => w("str")) .with({ type: "literal" }, (node) => w(`Literal["${node.value}"]`)) - .with( - { type: "record" }, - (node) => w(`dict[str, ${mapPythonType(node.valueType)}]`), + .with({ type: "record" }, (node) => + w(`dict[str, ${mapPythonType(node.valueType)}]`), ) .with({ type: "enum" }, (node) => { wn(`class ${node.name}(str, Enum):`); @@ -134,9 +127,10 @@ function generateNode(node: Node, writer: Writer) { if (m.name) { name = m.name; } else { - const ident = m.type === "object" - ? m.properties?.find((p) => p.required)?.key ?? "" - : ""; + const ident = + m.type === "object" + ? (m.properties?.find((p) => p.required)?.key ?? "") + : ""; name = `${node.name}${cap(ident)}`; } if (!generatedDependentClasses.has(name)) { @@ -148,7 +142,7 @@ function generateNode(node: Node, writer: Writer) { return name; }); writer.append(depWriter.output()); - wn(`${node.name} = Union[${classes.join(", ")}]`); + wn(`${node.name} = ${classes.join(" | ")}`); }) .with({ type: "object" }, (node) => { match(context.parent) @@ -156,9 +150,9 @@ function generateNode(node: Node, writer: Writer) { const name = context.closestName(); const ident = node.properties.find((p) => p.required)?.key ?? ""; wn( - `class ${name}${ - cap(ident) - }(msgspec.Struct, kw_only=True, omit_defaults=True):`, + `class ${name}${cap( + ident, + )}(msgspec.Struct, kw_only=True, omit_defaults=True):`, ); }) .with(P.nullish, () => { @@ -179,9 +173,8 @@ function generateNode(node: Node, writer: Writer) { for (const { key, required, description, value } of sortedProperties) { w(` ${key}: `); - if (!required) w("Union["); generateNode(value, writer); - if (!required) w(", None] = None"); + if (!required) w(" | None = None"); wn(""); if (description) { wn(` """${description}"""`); @@ -193,7 +186,6 @@ function generateNode(node: Node, writer: Writer) { const depWriter = new Writer(); const { w: d, wn: dn } = depWriter.shorthand(); const classes: string[] = []; - w("Union["); for (const [name, properties] of Object.entries(node.members)) { for (const { value } of properties) { if (isComplexType(value)) { @@ -213,15 +205,17 @@ function generateNode(node: Node, writer: Writer) { const sortedProperties = sortByRequired(properties); - for ( - const { key, required, description, value } of sortedProperties - ) { + for (const { + key, + required, + description, + value, + } of sortedProperties) { d(` ${key}: `); - if (!required) d("Union["); !isComplexType(value) ? generateNode(value, depWriter) : d(value.name ?? value.type); - if (!required) d(", None] = None"); + if (!required) d(" | None = None"); dn(""); if (description) { dn(` """${description}"""`); @@ -230,9 +224,9 @@ function generateNode(node: Node, writer: Writer) { dn(""); } } - w(classes.join(", ")); + w(classes.join(" | ")); writer.prepend(depWriter.output()); - wn("]"); + wn(""); }) .with({ type: "intersection" }, (node) => { assert( diff --git a/src/clients/python/src/justbe_webview/__init__.py b/src/clients/python/src/justbe_webview/__init__.py index 84651d8..8e273af 100644 --- a/src/clients/python/src/justbe_webview/__init__.py +++ b/src/clients/python/src/justbe_webview/__init__.py @@ -2,7 +2,7 @@ import os import platform import subprocess -from typing import Any, Callable, Literal, Union, cast, TypeVar +from typing import Any, Callable, Literal, cast, TypeVar from pathlib import Path import aiofiles import httpx @@ -54,7 +54,7 @@ def return_result( - result: Union[AckResponse, ResultResponse, ErrResponse], + result: AckResponse | ResultResponse | ErrResponse, expected_type: type[ResultType], ) -> Any: print(f"Return result: {result}") @@ -63,7 +63,7 @@ def return_result( raise ValueError(f"Expected {expected_type.__name__} result got: {result}") -def return_ack(result: Union[AckResponse, ResultResponse, ErrResponse]) -> None: +def return_ack(result: AckResponse | ResultResponse | ErrResponse) -> None: print(f"Return ack: {result}") if isinstance(result, AckResponse): return @@ -212,11 +212,11 @@ async def __aexit__( async def send(self, request: WebViewRequest) -> WebViewResponse: if self.process is None: raise RuntimeError("Webview process not started") - future: asyncio.Future[Union[AckResponse, ResultResponse, ErrResponse]] = ( + future: asyncio.Future[AckResponse | ResultResponse | ErrResponse] = ( asyncio.Future() ) - def set_result(event: Union[AckResponse, ResultResponse, ErrResponse]) -> None: + def set_result(event: AckResponse | ResultResponse | ErrResponse) -> None: future.set_result(event) self.internal_event.once(str(request.id), set_result) # type: ignore @@ -229,7 +229,7 @@ def set_result(event: Union[AckResponse, ResultResponse, ErrResponse]) -> None: result = await future return result - async def recv(self) -> Union[WebViewNotification, None]: + async def recv(self) -> WebViewNotification | None: if self.process is None: raise RuntimeError("Webview process not started") @@ -319,7 +319,7 @@ async def set_size(self, size: dict[Literal["width", "height"], float]): async def get_size( self, include_decorations: bool = False - ) -> dict[Literal["width", "height", "scaleFactor"], Union[int, float]]: + ) -> dict[Literal["width", "height", "scaleFactor"], int | float]: result = await self.send( GetSizeRequest(id=self.message_id, include_decorations=include_decorations) ) diff --git a/src/clients/python/src/justbe_webview/schemas.py b/src/clients/python/src/justbe_webview/schemas.py index e4f24a6..88f5ae2 100644 --- a/src/clients/python/src/justbe_webview/schemas.py +++ b/src/clients/python/src/justbe_webview/schemas.py @@ -1,6 +1,5 @@ # DO NOT EDIT: This file is auto-generated by generate-schema/index.ts from enum import Enum -from typing import Union import msgspec __all__ = [ @@ -60,7 +59,7 @@ class ClosedNotification(msgspec.Struct, tag_field="$type", tag="closed"): pass -Notification = Union[StartedNotification, IpcNotification, ClosedNotification] +Notification = StartedNotification | IpcNotification | ClosedNotification """ Messages that are sent unbidden from the webview to the client. """ @@ -91,7 +90,7 @@ class SizeResultType(msgspec.Struct, tag_field="$type", tag="size"): value: SizeWithScale -ResultType = Union[StringResultType, BooleanResultType, FloatResultType, SizeResultType] +ResultType = StringResultType | BooleanResultType | FloatResultType | SizeResultType """ Types that can be returned from webview results. """ @@ -111,7 +110,7 @@ class ErrResponse(msgspec.Struct, tag_field="$type", tag="err"): message: str -Response = Union[AckResponse, ResultResponse, ErrResponse] +Response = AckResponse | ResultResponse | ErrResponse """ Responses from the webview to the client. """ @@ -125,7 +124,7 @@ class ResponseMessage(msgspec.Struct, tag_field="$type", tag="response"): data: Response -Message = Union[NotificationMessage, ResponseMessage] +Message = NotificationMessage | ResponseMessage """ Complete definition of all outbound messages from the webview to the client. """ @@ -134,18 +133,18 @@ class ResponseMessage(msgspec.Struct, tag_field="$type", tag="response"): class ContentUrl(msgspec.Struct, kw_only=True, omit_defaults=True): url: str """Url to load in the webview. Note: Don't use data URLs here, as they are not supported. Use the `html` field instead.""" - headers: Union[dict[str, str], None] = None + headers: dict[str, str] | None = None """Optional headers to send with the request.""" class ContentHtml(msgspec.Struct, kw_only=True, omit_defaults=True): html: str """Html to load in the webview.""" - origin: Union[str, None] = None + origin: str | None = None """What to set as the origin of the webview when loading html.""" -Content = Union[ContentUrl, ContentHtml] +Content = ContentUrl | ContentHtml """ The content to load into the webview. """ @@ -163,7 +162,7 @@ class WindowSizeStates(str, Enum): fullscreen = "fullscreen" -WindowSize = Union[WindowSizeStates, Size] +WindowSize = WindowSizeStates | Size class Options(msgspec.Struct, omit_defaults=True): @@ -173,37 +172,37 @@ class Options(msgspec.Struct, omit_defaults=True): title: str """Sets the title of the window.""" - acceptFirstMouse: Union[bool, None] = None + acceptFirstMouse: bool | None = None """Sets whether clicking an inactive window also clicks through to the webview. Default is false.""" - autoplay: Union[bool, None] = None + autoplay: bool | None = None """When true, all media can be played without user interaction. Default is false.""" - clipboard: Union[bool, None] = None + clipboard: bool | None = None """Enables clipboard access for the page rendered on Linux and Windows. macOS doesn’t provide such method and is always enabled by default. But your app will still need to add menu item accelerators to use the clipboard shortcuts.""" - decorations: Union[bool, None] = None + decorations: bool | None = None """When true, the window will have a border, a title bar, etc. Default is true.""" - devtools: Union[bool, None] = None + devtools: bool | None = None """Enable or disable webview devtools. Note this only enables devtools to the webview. To open it, you can call `webview.open_devtools()`, or right click the page and open it from the context menu.""" - focused: Union[bool, None] = None + focused: bool | None = None """Sets whether the webview should be focused when created. Default is false.""" - incognito: Union[bool, None] = None + incognito: bool | None = None """Run the WebView with incognito mode. Note that WebContext will be ingored if incognito is enabled. Platform-specific: - Windows: Requires WebView2 Runtime version 101.0.1210.39 or higher, does nothing on older versions, see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/archive?tabs=dotnetcsharp#10121039""" - initializationScript: Union[str, None] = None + initializationScript: str | None = None """Run JavaScript code when loading new pages. When the webview loads a new page, this code will be executed. It is guaranteed that the code is executed before window.onload.""" - ipc: Union[bool, None] = None + ipc: bool | None = None """Sets whether host should be able to receive messages from the webview via `window.ipc.postMessage`.""" - load: Union[Content, None] = None + load: Content | None = None """The content to load into the webview.""" - size: Union[WindowSize, None] = None + size: WindowSize | None = None """The size of the window.""" - transparent: Union[bool, None] = None + transparent: bool | None = None """Sets whether the window should be transparent.""" - userAgent: Union[str, None] = None + userAgent: str | None = None """Sets the user agent to use when loading pages.""" @@ -251,7 +250,7 @@ class OpenDevToolsRequest(msgspec.Struct, tag_field="$type", tag="openDevTools") class GetSizeRequest(msgspec.Struct, tag_field="$type", tag="getSize"): id: int """The id of the request.""" - include_decorations: Union[bool, None] = None + include_decorations: bool | None = None """Whether to include the title bar and borders in the size measurement.""" @@ -265,21 +264,21 @@ class SetSizeRequest(msgspec.Struct, tag_field="$type", tag="setSize"): class FullscreenRequest(msgspec.Struct, tag_field="$type", tag="fullscreen"): id: int """The id of the request.""" - fullscreen: Union[bool, None] = None + fullscreen: bool | None = None """Whether to enter fullscreen mode. If left unspecified, the window will enter fullscreen mode if it is not already in fullscreen mode or exit fullscreen mode if it is currently in fullscreen mode.""" class MaximizeRequest(msgspec.Struct, tag_field="$type", tag="maximize"): id: int """The id of the request.""" - maximized: Union[bool, None] = None + maximized: bool | None = None """Whether to maximize the window. If left unspecified, the window will be maximized if it is not already maximized or restored if it was previously maximized.""" class MinimizeRequest(msgspec.Struct, tag_field="$type", tag="minimize"): id: int """The id of the request.""" - minimized: Union[bool, None] = None + minimized: bool | None = None """Whether to minimize the window. If left unspecified, the window will be minimized if it is not already minimized or restored if it was previously minimized.""" @@ -288,7 +287,7 @@ class LoadHtmlRequest(msgspec.Struct, tag_field="$type", tag="loadHtml"): """HTML to set as the content of the webview.""" id: int """The id of the request.""" - origin: Union[str, None] = None + origin: str | None = None """What to set as the origin of the webview when loading html. If not specified, the origin will be set to the value of the `origin` field when the webview was created.""" @@ -297,26 +296,26 @@ class LoadUrlRequest(msgspec.Struct, tag_field="$type", tag="loadUrl"): """The id of the request.""" url: str """URL to load in the webview.""" - headers: Union[dict[str, str], None] = None + headers: dict[str, str] | None = None """Optional headers to send with the request.""" -Request = Union[ - GetVersionRequest, - EvalRequest, - SetTitleRequest, - GetTitleRequest, - SetVisibilityRequest, - IsVisibleRequest, - OpenDevToolsRequest, - GetSizeRequest, - SetSizeRequest, - FullscreenRequest, - MaximizeRequest, - MinimizeRequest, - LoadHtmlRequest, - LoadUrlRequest, -] +Request = ( + GetVersionRequest + | EvalRequest + | SetTitleRequest + | GetTitleRequest + | SetVisibilityRequest + | IsVisibleRequest + | OpenDevToolsRequest + | GetSizeRequest + | SetSizeRequest + | FullscreenRequest + | MaximizeRequest + | MinimizeRequest + | LoadHtmlRequest + | LoadUrlRequest +) """ Explicit requests from the client to the webview. """