Skip to content

Commit 29b79d1

Browse files
committed
Conditionally whitelist datetime.datetime and add tests
1 parent b3f3662 commit 29b79d1

File tree

2 files changed

+71
-17
lines changed

2 files changed

+71
-17
lines changed

temporalio/worker/workflow_sandbox/_restrictions.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -953,19 +953,17 @@ def r_op(obj: Any, other: Any) -> Any:
953953

954954

955955
def _is_restrictable(v: Any) -> bool:
956-
return v is not None and not isinstance(
957-
v,
958-
(
959-
bool,
960-
int,
961-
float,
962-
complex,
963-
str,
964-
bytes,
965-
bytearray,
966-
datetime.date, # e.g. datetime.datetime
967-
),
968-
)
956+
do_not_restrict = [bool, int, float, complex, str, bytes, bytearray]
957+
if HAVE_PYDANTIC:
958+
# The datetime validator in pydantic_core
959+
# https://github.com/pydantic/pydantic-core/blob/741961c05847d9e9ee517cd783e24c2b58e5596b/src/input/input_python.rs#L548-L582
960+
# does some runtime type inspection that a RestrictedProxy instance
961+
# fails. For this reason we do not restrict date/datetime instances when
962+
# Pydantic is being used. Other restricted types such as pathlib.Path
963+
# and uuid.UUID which are likely to be used in Pydantic model fields
964+
# currently pass Pydantic's validation when wrapped by RestrictedProxy.
965+
do_not_restrict.append(datetime.date) # e.g. datetime.datetime
966+
return v is not None and not isinstance(v, tuple(do_not_restrict))
969967

970968

971969
class _RestrictedProxy:

tests/contrib/pydantic/test_pydantic.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import dataclasses
2+
import datetime
3+
import os
4+
import pathlib
25
import uuid
3-
from datetime import datetime
46

57
import pydantic
68
import pytest
@@ -9,6 +11,11 @@
911
from temporalio.client import Client
1012
from temporalio.contrib.pydantic import pydantic_data_converter
1113
from temporalio.worker import Worker
14+
from temporalio.worker.workflow_sandbox._restrictions import (
15+
RestrictionContext,
16+
SandboxMatcher,
17+
_RestrictedProxy,
18+
)
1219
from tests.contrib.pydantic.models import (
1320
PydanticModels,
1421
PydanticModelWithStrictField,
@@ -103,7 +110,7 @@ async def test_round_trip_misc_objects(client: Client):
103110
{"7": 7.0},
104111
[{"7": 7.0}],
105112
({"7": 7.0},),
106-
datetime(2025, 1, 2, 3, 4, 5),
113+
datetime.datetime(2025, 1, 2, 3, 4, 5),
107114
uuid.uuid4(),
108115
)
109116

@@ -262,7 +269,9 @@ async def test_datetime_usage_in_workflow(client: Client):
262269

263270
def test_pydantic_model_with_strict_field_outside_sandbox():
264271
_test_pydantic_model_with_strict_field(
265-
PydanticModelWithStrictField(strict_field=datetime(2025, 1, 2, 3, 4, 5))
272+
PydanticModelWithStrictField(
273+
strict_field=datetime.datetime(2025, 1, 2, 3, 4, 5)
274+
)
266275
)
267276

268277

@@ -276,7 +285,9 @@ async def test_pydantic_model_with_strict_field_inside_sandbox(client: Client):
276285
workflows=[PydanticModelWithStrictFieldWorkflow],
277286
task_queue=tq,
278287
):
279-
orig = PydanticModelWithStrictField(strict_field=datetime(2025, 1, 2, 3, 4, 5))
288+
orig = PydanticModelWithStrictField(
289+
strict_field=datetime.datetime(2025, 1, 2, 3, 4, 5)
290+
)
280291
result = await client.execute_workflow(
281292
PydanticModelWithStrictFieldWorkflow.run,
282293
orig,
@@ -324,3 +335,48 @@ async def test_validation_error(client: Client):
324335
task_queue=task_queue_name,
325336
result_type=tuple[int],
326337
)
338+
339+
340+
class RestrictedProxyFieldsModel(BaseModel):
341+
path_field: pathlib.Path
342+
uuid_field: uuid.UUID
343+
# A datetime.datetime field does not pass this test. See note in
344+
# temporalio.worker.workflow_sandbox._restrictions._is_restrictable
345+
datetime_field: datetime.datetime
346+
347+
348+
def test_model_instantiation_from_restricted_proxy_values():
349+
restricted_path_cls = _RestrictedProxy(
350+
"Path",
351+
pathlib.Path,
352+
RestrictionContext(),
353+
SandboxMatcher(),
354+
)
355+
restricted_uuid_cls = _RestrictedProxy(
356+
"uuid",
357+
uuid.UUID,
358+
RestrictionContext(),
359+
SandboxMatcher(),
360+
)
361+
restricted_datetime_cls = _RestrictedProxy(
362+
"datetime",
363+
datetime.datetime,
364+
RestrictionContext(),
365+
SandboxMatcher(),
366+
)
367+
368+
restricted_path = restricted_path_cls("test/path")
369+
restricted_uuid = restricted_uuid_cls(bytes=os.urandom(16), version=4)
370+
restricted_datetime = restricted_datetime_cls(2025, 1, 2, 3, 4, 5)
371+
372+
assert type(restricted_path) is _RestrictedProxy
373+
assert type(restricted_uuid) is _RestrictedProxy
374+
assert type(restricted_datetime) is not _RestrictedProxy
375+
p = RestrictedProxyFieldsModel(
376+
path_field=restricted_path, # type: ignore
377+
uuid_field=restricted_uuid, # type: ignore
378+
datetime_field=restricted_datetime, # type: ignore
379+
)
380+
assert p.path_field == restricted_path
381+
assert p.uuid_field == restricted_uuid
382+
assert p.datetime_field == restricted_datetime

0 commit comments

Comments
 (0)