Skip to content

Commit d3fa0e9

Browse files
committed
refactor(RemoveABC): favoring protocols
Allow ducktyping!
1 parent 4e7781f commit d3fa0e9

File tree

8 files changed

+115
-119
lines changed

8 files changed

+115
-119
lines changed

src/uipath/runtime/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
"""UiPath Runtime Package."""
22

33
from uipath.runtime.base import (
4-
UiPathBaseRuntime,
54
UiPathExecuteOptions,
65
UiPathExecutionRuntime,
6+
UiPathRuntimeProtocol,
77
UiPathStreamNotSupportedError,
88
UiPathStreamOptions,
99
)
1010
from uipath.runtime.context import UiPathRuntimeContext
1111
from uipath.runtime.events import UiPathRuntimeEvent
12-
from uipath.runtime.factory import UiPathRuntimeFactory
12+
from uipath.runtime.factory import UiPathRuntimeCreator, UiPathRuntimeScanner
1313
from uipath.runtime.result import (
1414
UiPathApiTrigger,
1515
UiPathBreakpointResult,
@@ -23,9 +23,10 @@
2323
"UiPathExecuteOptions",
2424
"UiPathStreamOptions",
2525
"UiPathRuntimeContext",
26-
"UiPathBaseRuntime",
26+
"UiPathRuntimeProtocol",
2727
"UiPathExecutionRuntime",
28-
"UiPathRuntimeFactory",
28+
"UiPathRuntimeCreator",
29+
"UiPathRuntimeScanner",
2930
"UiPathRuntimeResult",
3031
"UiPathRuntimeStatus",
3132
"UiPathRuntimeEvent",

src/uipath/runtime/base.py

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
"""Base runtime class and async context manager implementation."""
22

33
import logging
4-
from abc import ABC, abstractmethod
4+
import typing
55
from typing import (
66
Any,
77
AsyncGenerator,
8-
Generic,
98
Literal,
109
Optional,
11-
TypeVar,
1210
)
1311

1412
from pydantic import BaseModel, Field
15-
from typing_extensions import override
1613
from uipath.core import UiPathTraceManager
1714

1815
from uipath.runtime.events import (
@@ -55,27 +52,20 @@ class UiPathStreamOptions(UiPathExecuteOptions):
5552
pass
5653

5754

58-
class UiPathBaseRuntime(ABC):
59-
"""Base runtime class implementing the async context manager protocol.
55+
class UiPathExecutable(typing.Protocol):
56+
"""UiPath execution interface."""
6057

61-
This allows using the class with 'async with' statements.
62-
"""
63-
64-
async def get_schema(self) -> UiPathRuntimeSchema:
65-
"""Get schema for this runtime.
66-
67-
Returns: The runtime's schema (entrypoint type, input/output json schema).
68-
"""
69-
raise NotImplementedError()
70-
71-
@abstractmethod
7258
async def execute(
7359
self,
7460
input: Optional[dict[str, Any]] = None,
7561
options: Optional[UiPathExecuteOptions] = None,
7662
) -> UiPathRuntimeResult:
7763
"""Produce the agent output."""
78-
raise NotImplementedError()
64+
...
65+
66+
67+
class UiPathStreamable(typing.Protocol):
68+
"""UiPath streaming interface."""
7969

8070
async def stream(
8171
self,
@@ -120,21 +110,35 @@ async def stream(
120110
# Without it, the function wouldn't match the AsyncGenerator return type
121111
yield
122112

123-
@abstractmethod
124-
async def cleanup(self):
125-
"""Cleaup runtime resources."""
126-
pass
127113

114+
class HasSchema(typing.Protocol):
115+
"""Contains runtime input and output schema."""
128116

129-
T = TypeVar("T", bound=UiPathBaseRuntime)
117+
async def get_schema(self) -> UiPathRuntimeSchema:
118+
"""Get schema for a runtime.
119+
120+
Returns: The runtime's schema (entrypoint type, input/output json schema).
121+
"""
122+
...
130123

131124

132-
class UiPathExecutionRuntime(UiPathBaseRuntime, Generic[T]):
125+
# Note: explicitly marking it as a protocol for mypy.
126+
# https://mypy.readthedocs.io/en/stable/protocols.html#defining-subprotocols-and-subclassing-protocols
127+
# Note that inheriting from an existing protocol does not automatically turn the subclass into a protocol
128+
# – it just creates a regular (non-protocol) class or ABC that implements the given protocol (or protocols).
129+
# The Protocol base class must always be explicitly present if you are defining a protocol.
130+
class UiPathRuntimeProtocol(
131+
UiPathExecutable, UiPathStreamable, HasSchema, typing.Protocol
132+
):
133+
"""UiPath Runtime Protocol."""
134+
135+
136+
class UiPathExecutionRuntime:
133137
"""Handles runtime execution with tracing/telemetry."""
134138

135139
def __init__(
136140
self,
137-
delegate: T,
141+
delegate: UiPathRuntimeProtocol,
138142
trace_manager: UiPathTraceManager,
139143
root_span: str = "root",
140144
log_handler: Optional[UiPathRuntimeExecutionLogHandler] = None,
@@ -174,7 +178,6 @@ async def execute(
174178
if self.log_handler:
175179
log_interceptor.teardown()
176180

177-
@override
178181
async def stream(
179182
self,
180183
input: Optional[dict[str, Any]] = None,
@@ -209,6 +212,6 @@ async def stream(
209212
if self.log_handler:
210213
log_interceptor.teardown()
211214

212-
def cleanup(self) -> None:
213-
"""Close runtime resources."""
214-
pass
215+
async def get_schema(self) -> UiPathRuntimeSchema:
216+
"""Passthrough schema for the delegate."""
217+
return await self.delegate.get_schema()
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""Initialization module for the debug package."""
22

3-
from uipath.runtime.debug.bridge import UiPathDebugBridge
3+
from uipath.runtime.debug.bridge import UiPathDebugBridgeProtocol
44
from uipath.runtime.debug.exception import (
55
UiPathDebugQuitError,
66
)
77
from uipath.runtime.debug.runtime import UiPathDebugRuntime
88

99
__all__ = [
1010
"UiPathDebugQuitError",
11-
"UiPathDebugBridge",
11+
"UiPathDebugBridgeProtocol",
1212
"UiPathDebugRuntime",
1313
]

src/uipath/runtime/debug/bridge.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Abstract debug bridge interface."""
22

3-
from abc import ABC, abstractmethod
4-
from typing import Any, Literal
3+
from typing import Any, Literal, Protocol
54

65
from uipath.runtime import (
76
UiPathBreakpointResult,
@@ -10,65 +9,56 @@
109
from uipath.runtime.events import UiPathRuntimeStateEvent
1110

1211

13-
class UiPathDebugBridge(ABC):
12+
class UiPathDebugBridgeProtocol(Protocol):
1413
"""Abstract interface for debug communication.
1514
1615
Implementations: SignalR, Console, WebSocket, etc.
1716
"""
1817

19-
@abstractmethod
2018
async def connect(self) -> None:
2119
"""Establish connection to debugger."""
22-
pass
20+
...
2321

24-
@abstractmethod
2522
async def disconnect(self) -> None:
2623
"""Close connection to debugger."""
27-
pass
24+
...
2825

29-
@abstractmethod
3026
async def emit_execution_started(self, **kwargs) -> None:
3127
"""Notify debugger that execution started."""
32-
pass
28+
...
3329

34-
@abstractmethod
3530
async def emit_state_update(self, state_event: UiPathRuntimeStateEvent) -> None:
3631
"""Notify debugger of runtime state update."""
37-
pass
32+
...
3833

39-
@abstractmethod
4034
async def emit_breakpoint_hit(
4135
self, breakpoint_result: UiPathBreakpointResult
4236
) -> None:
4337
"""Notify debugger that a breakpoint was hit."""
44-
pass
38+
...
4539

46-
@abstractmethod
4740
async def emit_execution_completed(
4841
self,
4942
runtime_result: UiPathRuntimeResult,
5043
) -> None:
5144
"""Notify debugger that execution completed."""
52-
pass
45+
...
5346

54-
@abstractmethod
5547
async def emit_execution_error(
5648
self,
5749
error: str,
5850
) -> None:
5951
"""Notify debugger that an error occurred."""
60-
pass
52+
...
6153

62-
@abstractmethod
6354
async def wait_for_resume(self) -> Any:
6455
"""Wait for resume command from debugger."""
65-
pass
56+
...
6657

67-
@abstractmethod
6858
def get_breakpoints(self) -> list[str] | Literal["*"]:
6959
"""Get nodes to suspend execution at.
7060
7161
Returns:
7262
List of node names to suspend at, or ["*"] for all nodes (step mode)
7363
"""
74-
pass
64+
...

src/uipath/runtime/debug/runtime.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,38 @@
11
"""Debug runtime implementation."""
22

33
import logging
4-
from typing import Any, Generic, Optional, TypeVar
4+
from typing import Any, Optional
55

66
from uipath.runtime import (
7-
UiPathBaseRuntime,
87
UiPathBreakpointResult,
98
UiPathExecuteOptions,
9+
UiPathRuntimeProtocol,
1010
UiPathRuntimeResult,
1111
UiPathRuntimeStatus,
1212
UiPathStreamNotSupportedError,
1313
UiPathStreamOptions,
1414
)
15-
from uipath.runtime.debug import UiPathDebugBridge, UiPathDebugQuitError
15+
from uipath.runtime.debug import UiPathDebugBridgeProtocol, UiPathDebugQuitError
1616
from uipath.runtime.events import (
1717
UiPathRuntimeStateEvent,
1818
)
19+
from uipath.runtime.schema import UiPathRuntimeSchema
1920

2021
logger = logging.getLogger(__name__)
2122

22-
T = TypeVar("T", bound=UiPathBaseRuntime)
2323

24-
25-
class UiPathDebugRuntime(UiPathBaseRuntime, Generic[T]):
24+
class UiPathDebugRuntime:
2625
"""Specialized runtime for debug runs that streams events to a debug bridge."""
2726

2827
def __init__(
2928
self,
30-
delegate: T,
31-
debug_bridge: UiPathDebugBridge,
29+
delegate: UiPathRuntimeProtocol,
30+
debug_bridge: UiPathDebugBridgeProtocol,
3231
):
3332
"""Initialize the UiPathDebugRuntime."""
3433
super().__init__()
35-
self.delegate: T = delegate
36-
self.debug_bridge: UiPathDebugBridge = debug_bridge
34+
self.delegate = delegate
35+
self.debug_bridge: UiPathDebugBridgeProtocol = debug_bridge
3736

3837
async def execute(
3938
self,
@@ -129,3 +128,7 @@ async def cleanup(self) -> None:
129128
await self.debug_bridge.disconnect()
130129
except Exception as e:
131130
logger.warning(f"Error disconnecting debug bridge: {e}")
131+
132+
async def get_schema(self) -> UiPathRuntimeSchema:
133+
"""Passthrough schema for the delegate."""
134+
return await self.delegate.get_schema()

src/uipath/runtime/factory.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
"""Factory for creating UiPath runtime instances."""
1+
"""Protocols for creating UiPath runtime instances."""
22

3-
from abc import ABC, abstractmethod
4-
from typing import (
5-
Generic,
6-
TypeVar,
7-
)
3+
from typing import Protocol
84

9-
from uipath.runtime.base import UiPathBaseRuntime
5+
from uipath.runtime.base import UiPathRuntimeProtocol
106

11-
T = TypeVar("T", bound=UiPathBaseRuntime)
127

8+
class UiPathRuntimeScanner(Protocol):
9+
"""Protocol for discovering all UiPath runtime instances."""
1310

14-
class UiPathRuntimeFactory(Generic[T], ABC):
15-
"""Generic factory for UiPath runtime classes."""
16-
17-
@abstractmethod
18-
def discover_runtimes(self) -> list[T]:
11+
def discover_runtimes(self) -> list[UiPathRuntimeProtocol]:
1912
"""Discover all runtime classes."""
20-
raise NotImplementedError()
13+
...
14+
15+
16+
class UiPathRuntimeCreator(Protocol):
17+
"""Protocol for creating a UiPath runtime given an entrypoint."""
2118

22-
@abstractmethod
23-
def new_runtime(self, entrypoint: str) -> T:
19+
def new_runtime(self, entrypoint: str) -> UiPathRuntimeProtocol:
2420
"""Create a new runtime instance."""
25-
raise NotImplementedError()
21+
...

0 commit comments

Comments
 (0)