pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501140808__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglove/core/__init__.py +54 -20
- pyglove/core/coding/__init__.py +42 -0
- pyglove/core/coding/errors.py +111 -0
- pyglove/core/coding/errors_test.py +98 -0
- pyglove/core/coding/execution.py +309 -0
- pyglove/core/coding/execution_test.py +333 -0
- pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
- pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
- pyglove/core/coding/parsing.py +153 -0
- pyglove/core/coding/parsing_test.py +150 -0
- pyglove/core/coding/permissions.py +100 -0
- pyglove/core/coding/permissions_test.py +93 -0
- pyglove/core/geno/base.py +54 -41
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +37 -28
- pyglove/core/geno/custom.py +19 -16
- pyglove/core/geno/numerical.py +20 -17
- pyglove/core/geno/space.py +4 -5
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +94 -55
- pyglove/core/hyper/custom.py +7 -7
- pyglove/core/hyper/custom_test.py +9 -10
- pyglove/core/hyper/derived.py +30 -22
- pyglove/core/hyper/derived_test.py +2 -4
- pyglove/core/hyper/dynamic_evaluation.py +5 -6
- pyglove/core/hyper/evolvable.py +57 -46
- pyglove/core/hyper/numerical.py +48 -24
- pyglove/core/hyper/numerical_test.py +9 -9
- pyglove/core/hyper/object_template.py +58 -46
- pyglove/core/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +17 -7
- pyglove/core/io/file_system_test.py +2 -0
- pyglove/core/io/sequence.py +299 -0
- pyglove/core/io/sequence_test.py +124 -0
- pyglove/core/logging_test.py +0 -2
- pyglove/core/patching/object_factory.py +4 -4
- pyglove/core/patching/pattern_based.py +4 -4
- pyglove/core/patching/rule_based.py +17 -5
- pyglove/core/patching/rule_based_test.py +27 -4
- pyglove/core/symbolic/__init__.py +2 -7
- pyglove/core/symbolic/base.py +320 -183
- pyglove/core/symbolic/base_test.py +123 -19
- pyglove/core/symbolic/boilerplate.py +7 -13
- pyglove/core/symbolic/boilerplate_test.py +25 -23
- pyglove/core/symbolic/class_wrapper.py +48 -45
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +9 -15
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/dict.py +154 -110
- pyglove/core/symbolic/dict_test.py +238 -130
- pyglove/core/symbolic/diff.py +199 -10
- pyglove/core/symbolic/diff_test.py +226 -0
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +29 -26
- pyglove/core/symbolic/functor_test.py +102 -50
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +81 -50
- pyglove/core/symbolic/list_test.py +119 -97
- pyglove/core/symbolic/object.py +225 -113
- pyglove/core/symbolic/object_test.py +320 -108
- pyglove/core/symbolic/origin.py +17 -14
- pyglove/core/symbolic/origin_test.py +4 -2
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +108 -21
- pyglove/core/symbolic/ref_test.py +93 -0
- pyglove/core/symbolic/symbolize_test.py +10 -2
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/tuning/sample_test.py +3 -3
- pyglove/core/typing/__init__.py +14 -5
- pyglove/core/typing/annotation_conversion.py +43 -27
- pyglove/core/typing/annotation_conversion_test.py +23 -0
- pyglove/core/typing/callable_ext.py +241 -3
- pyglove/core/typing/callable_ext_test.py +255 -0
- pyglove/core/typing/callable_signature.py +510 -66
- pyglove/core/typing/callable_signature_test.py +619 -99
- pyglove/core/typing/class_schema.py +229 -154
- pyglove/core/typing/class_schema_test.py +149 -95
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/inspect.py +63 -0
- pyglove/core/typing/inspect_test.py +39 -0
- pyglove/core/typing/key_specs.py +10 -11
- pyglove/core/typing/key_specs_test.py +7 -4
- pyglove/core/typing/type_conversion.py +4 -5
- pyglove/core/typing/type_conversion_test.py +12 -12
- pyglove/core/typing/typed_missing.py +6 -7
- pyglove/core/typing/typed_missing_test.py +7 -8
- pyglove/core/typing/value_specs.py +604 -362
- pyglove/core/typing/value_specs_test.py +328 -90
- pyglove/core/utils/__init__.py +164 -0
- pyglove/core/{object_utils → utils}/common_traits.py +3 -67
- pyglove/core/utils/common_traits_test.py +36 -0
- pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
- pyglove/core/{object_utils → utils}/error_utils.py +78 -9
- pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
- pyglove/core/utils/formatting.py +464 -0
- pyglove/core/utils/formatting_test.py +453 -0
- pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
- pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
- pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
- pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
- pyglove/core/{object_utils → utils}/missing.py +3 -3
- pyglove/core/{object_utils → utils}/missing_test.py +2 -4
- pyglove/core/utils/text_color.py +128 -0
- pyglove/core/utils/text_color_test.py +94 -0
- pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
- pyglove/core/utils/timing.py +236 -0
- pyglove/core/utils/timing_test.py +154 -0
- pyglove/core/{object_utils → utils}/value_location.py +275 -6
- pyglove/core/utils/value_location_test.py +707 -0
- pyglove/core/views/__init__.py +32 -0
- pyglove/core/views/base.py +804 -0
- pyglove/core/views/base_test.py +580 -0
- pyglove/core/views/html/__init__.py +27 -0
- pyglove/core/views/html/base.py +547 -0
- pyglove/core/views/html/base_test.py +830 -0
- pyglove/core/views/html/controls/__init__.py +35 -0
- pyglove/core/views/html/controls/base.py +275 -0
- pyglove/core/views/html/controls/label.py +207 -0
- pyglove/core/views/html/controls/label_test.py +157 -0
- pyglove/core/views/html/controls/progress_bar.py +183 -0
- pyglove/core/views/html/controls/progress_bar_test.py +97 -0
- pyglove/core/views/html/controls/tab.py +320 -0
- pyglove/core/views/html/controls/tab_test.py +87 -0
- pyglove/core/views/html/controls/tooltip.py +99 -0
- pyglove/core/views/html/controls/tooltip_test.py +99 -0
- pyglove/core/views/html/tree_view.py +1517 -0
- pyglove/core/views/html/tree_view_test.py +1461 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/METADATA +18 -4
- pyglove-0.4.5.dev202501140808.dist-info/RECORD +214 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -154
- pyglove/core/object_utils/common_traits_test.py +0 -82
- pyglove/core/object_utils/formatting.py +0 -234
- pyglove/core/object_utils/formatting_test.py +0 -223
- pyglove/core/object_utils/value_location_test.py +0 -385
- pyglove/core/symbolic/schema_utils.py +0 -327
- pyglove/core/symbolic/schema_utils_test.py +0 -57
- pyglove/core/typing/class_schema_utils.py +0 -202
- pyglove/core/typing/class_schema_utils_test.py +0 -194
- pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/top_level.txt +0 -0
@@ -16,13 +16,14 @@
|
|
16
16
|
import abc
|
17
17
|
import base64
|
18
18
|
import collections
|
19
|
+
import contextlib
|
19
20
|
import importlib
|
20
21
|
import inspect
|
21
22
|
import marshal
|
22
23
|
import pickle
|
23
24
|
import types
|
24
25
|
import typing
|
25
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union
|
26
|
+
from typing import Any, Callable, ContextManager, Dict, Iterable, Iterator, List, Optional, Set, Tuple, Type, TypeVar, Union
|
26
27
|
|
27
28
|
# Nestable[T] is a (maybe) nested structure of T, which could be T, a Dict
|
28
29
|
# a List or a Tuple of Nestable[T]. We use a Union to fool PyType checker to
|
@@ -36,7 +37,7 @@ JSONPrimitiveType = Union[int, float, bool, str]
|
|
36
37
|
# pytype doesn't support recursion. Use Any instead of 'JSONValueType'
|
37
38
|
# in List and Dict.
|
38
39
|
JSONListType = List[Any]
|
39
|
-
JSONDictType = Dict[str, Any]
|
40
|
+
JSONDictType = Dict[Union[str, int], Any]
|
40
41
|
JSONValueType = Union[JSONPrimitiveType, JSONListType, JSONDictType]
|
41
42
|
|
42
43
|
# pylint: enable=invalid-name
|
@@ -57,6 +58,7 @@ class _TypeRegistry:
|
|
57
58
|
# registered for a user class.
|
58
59
|
self._type_to_cls_map = dict()
|
59
60
|
self._prefix_mapping = dict()
|
61
|
+
self._ondemand_registry_stack = []
|
60
62
|
|
61
63
|
def register(
|
62
64
|
self, type_name: str, cls: Type[Any], override_existing: bool = False
|
@@ -89,9 +91,32 @@ class _TypeRegistry:
|
|
89
91
|
"""Returns whether a type name is registered."""
|
90
92
|
return type_name in self._type_to_cls_map
|
91
93
|
|
94
|
+
@contextlib.contextmanager
|
95
|
+
def load_types_for_deserialization(
|
96
|
+
self,
|
97
|
+
*types_to_deserialize: Type[Any],
|
98
|
+
) -> Iterator[Dict[str, Type[Any]]]:
|
99
|
+
"""Context manager for loading unregistered types for deserialization."""
|
100
|
+
if self._ondemand_registry_stack:
|
101
|
+
stack_top = dict(self._ondemand_registry_stack[-1])
|
102
|
+
else:
|
103
|
+
stack_top = {}
|
104
|
+
stack_top.update({t.__name__: t for t in types_to_deserialize})
|
105
|
+
try:
|
106
|
+
self._ondemand_registry_stack.append(stack_top)
|
107
|
+
yield stack_top
|
108
|
+
finally:
|
109
|
+
self._ondemand_registry_stack.pop()
|
110
|
+
|
92
111
|
def class_from_typename(
|
93
112
|
self, type_name: str) -> Optional[Type[Any]]:
|
94
113
|
"""Get class from type name."""
|
114
|
+
if self._ondemand_registry_stack:
|
115
|
+
top_registry = self._ondemand_registry_stack[-1]
|
116
|
+
class_name = type_name.split('.')[-1]
|
117
|
+
if class_name in top_registry:
|
118
|
+
return top_registry[class_name]
|
119
|
+
|
95
120
|
cls = self._type_to_cls_map.get(type_name, None)
|
96
121
|
if cls is None:
|
97
122
|
# Modules could be renamed, to load legacy serialized objects, we
|
@@ -177,9 +202,8 @@ class JSONConvertible(metaclass=abc.ABCMeta):
|
|
177
202
|
Returns:
|
178
203
|
An instance of cls.
|
179
204
|
"""
|
180
|
-
del kwargs
|
181
205
|
assert isinstance(json_value, dict)
|
182
|
-
init_args = {k: from_json(v) for k, v in json_value.items()
|
206
|
+
init_args = {k: from_json(v, **kwargs) for k, v in json_value.items()
|
183
207
|
if k != JSONConvertible.TYPE_NAME_KEY}
|
184
208
|
return cls(**init_args)
|
185
209
|
|
@@ -249,6 +273,36 @@ class JSONConvertible(metaclass=abc.ABCMeta):
|
|
249
273
|
"""Returns an iterator of registered (serialization key, class) tuples."""
|
250
274
|
return cls._TYPE_REGISTRY.iteritems()
|
251
275
|
|
276
|
+
@classmethod
|
277
|
+
def load_types_for_deserialization(
|
278
|
+
cls,
|
279
|
+
*types_to_deserialize: Type[Any]
|
280
|
+
) -> ContextManager[Dict[str, Type[Any]]]:
|
281
|
+
"""Context manager for loading unregistered types for deserialization.
|
282
|
+
|
283
|
+
Example::
|
284
|
+
|
285
|
+
class A(pg.Object):
|
286
|
+
auto_register = False
|
287
|
+
x: int
|
288
|
+
|
289
|
+
class B(A):
|
290
|
+
y: str
|
291
|
+
with pg.JSONConvertile.load_types_for_deserialization(A, B):
|
292
|
+
pg.from_json_str(A(1).to_json_str())
|
293
|
+
pg.from_json_str(B(1, 'hi').to_json_str())
|
294
|
+
|
295
|
+
Args:
|
296
|
+
*types_to_deserialize: A list of types to be loaded for deserialization.
|
297
|
+
|
298
|
+
Returns:
|
299
|
+
A context manager within which the objects of the requested types
|
300
|
+
could be deserialized.
|
301
|
+
"""
|
302
|
+
return cls._TYPE_REGISTRY.load_types_for_deserialization(
|
303
|
+
*types_to_deserialize
|
304
|
+
)
|
305
|
+
|
252
306
|
@classmethod
|
253
307
|
def to_json_dict(
|
254
308
|
cls,
|
@@ -355,54 +409,68 @@ def to_json(value: Any, **kwargs) -> Any:
|
|
355
409
|
JSON value.
|
356
410
|
"""
|
357
411
|
if isinstance(value, (type(None), bool, int, float, str)):
|
358
|
-
|
412
|
+
v = value
|
359
413
|
elif isinstance(value, JSONConvertible):
|
360
|
-
|
414
|
+
v = value.to_json(**kwargs)
|
361
415
|
elif isinstance(value, tuple):
|
362
|
-
|
416
|
+
v = [JSONConvertible.TUPLE_MARKER] + to_json(list(value), **kwargs)
|
363
417
|
elif isinstance(value, list):
|
364
|
-
|
418
|
+
v = [to_json(item, **kwargs) for item in value]
|
365
419
|
elif isinstance(value, dict):
|
366
|
-
|
420
|
+
v = {k: to_json(v, **kwargs) for k, v in value.items()}
|
367
421
|
elif isinstance(value, (type, typing.GenericAlias)): # pytype: disable=module-attr
|
368
|
-
|
422
|
+
v = _type_to_json(value)
|
369
423
|
elif inspect.isbuiltin(value):
|
370
|
-
|
424
|
+
v = _builtin_function_to_json(value)
|
371
425
|
elif inspect.isfunction(value):
|
372
|
-
|
426
|
+
v = _function_to_json(value)
|
373
427
|
elif inspect.ismethod(value):
|
374
|
-
|
428
|
+
v = _method_to_json(value)
|
375
429
|
# pytype: disable=module-attr
|
376
430
|
elif isinstance(value, typing._Final): # pylint: disable=protected-access
|
377
431
|
# pytype: enable=module-attr
|
378
|
-
|
432
|
+
v = _annotation_to_json(value)
|
379
433
|
elif value is ...:
|
380
|
-
|
434
|
+
v = {JSONConvertible.TYPE_NAME_KEY: 'type', 'name': 'builtins.Ellipsis'}
|
381
435
|
else:
|
436
|
+
v, converted = None, False
|
382
437
|
if JSONConvertible.TYPE_CONVERTER is not None:
|
383
438
|
converter = JSONConvertible.TYPE_CONVERTER(type(value)) # pylint: disable=not-callable
|
384
439
|
if converter:
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
440
|
+
v = to_json(converter(value))
|
441
|
+
converted = True
|
442
|
+
if not converted:
|
443
|
+
v = _OpaqueObject(value).to_json(**kwargs)
|
444
|
+
return v
|
445
|
+
|
446
|
+
|
447
|
+
def from_json(
|
448
|
+
json_value: JSONValueType,
|
449
|
+
*,
|
450
|
+
auto_import: bool = True,
|
451
|
+
auto_dict: bool = False,
|
452
|
+
**kwargs) -> Any:
|
393
453
|
"""Deserializes a (maybe) JSONConvertible value from JSON value.
|
394
454
|
|
395
455
|
Args:
|
396
456
|
json_value: Input JSON value.
|
397
|
-
|
398
|
-
|
457
|
+
auto_import: If True, when a '_type' is not registered, PyGlove will
|
458
|
+
identify its parent module and automatically import it. For example,
|
459
|
+
if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
|
460
|
+
find the class 'A' within the imported module.
|
461
|
+
auto_dict: If True, dict with '_type' that cannot be loaded will remain
|
462
|
+
as dict, with '_type' renamed to 'type_name'.
|
399
463
|
**kwargs: Keyword arguments that will be passed to JSONConvertible.__init__.
|
400
464
|
|
401
465
|
Returns:
|
402
466
|
Deserialized value.
|
403
467
|
"""
|
404
|
-
|
405
|
-
|
468
|
+
typename_resolved = kwargs.pop('_typename_resolved', False)
|
469
|
+
if not typename_resolved:
|
470
|
+
json_value = resolve_typenames(json_value, auto_import, auto_dict)
|
471
|
+
|
472
|
+
def child_from(v):
|
473
|
+
return from_json(v, _typename_resolved=True, **kwargs)
|
406
474
|
|
407
475
|
if isinstance(json_value, list):
|
408
476
|
if json_value and json_value[0] == JSONConvertible.TUPLE_MARKER:
|
@@ -411,41 +479,89 @@ def from_json(json_value: JSONValueType,
|
|
411
479
|
f'Tuple should have at least one element '
|
412
480
|
f'besides \'{JSONConvertible.TUPLE_MARKER}\'. '
|
413
481
|
f'Encountered: {json_value}.')
|
414
|
-
return tuple([
|
415
|
-
return [
|
482
|
+
return tuple([child_from(v) for v in json_value[1:]])
|
483
|
+
return [child_from(v) for v in json_value]
|
416
484
|
elif isinstance(json_value, dict):
|
417
485
|
if JSONConvertible.TYPE_NAME_KEY not in json_value:
|
418
|
-
return {k:
|
419
|
-
|
486
|
+
return {k: child_from(v) for k, v in json_value.items()}
|
487
|
+
factory_fn = json_value.pop(JSONConvertible.TYPE_NAME_KEY)
|
488
|
+
assert factory_fn is not None
|
489
|
+
return factory_fn(json_value, **kwargs)
|
490
|
+
return json_value
|
491
|
+
|
492
|
+
|
493
|
+
def resolve_typenames(
|
494
|
+
json_value: JSONValueType,
|
495
|
+
auto_import: bool = True,
|
496
|
+
auto_dict: bool = False
|
497
|
+
) -> JSONValueType:
|
498
|
+
"""Inplace resolves the "_type" keys with their factories in a JSON tree."""
|
499
|
+
|
500
|
+
def _resolve_typename(v: Dict[str, Any]) -> bool:
|
501
|
+
"""Returns True if the subtree is resolved for the first time."""
|
502
|
+
if JSONConvertible.TYPE_NAME_KEY not in v:
|
503
|
+
return True
|
504
|
+
if not isinstance(v[JSONConvertible.TYPE_NAME_KEY], str):
|
505
|
+
return False
|
506
|
+
type_name = v[JSONConvertible.TYPE_NAME_KEY]
|
420
507
|
if type_name == 'type':
|
421
|
-
|
508
|
+
factory_fn = _type_from_json
|
422
509
|
elif type_name == 'function':
|
423
|
-
|
510
|
+
factory_fn = _function_from_json
|
424
511
|
elif type_name == 'method':
|
425
|
-
|
512
|
+
factory_fn = _method_from_json
|
426
513
|
else:
|
427
514
|
cls = JSONConvertible.class_from_typename(type_name)
|
428
515
|
if cls is None:
|
516
|
+
if auto_import:
|
517
|
+
try:
|
518
|
+
cls = _load_symbol(type_name)
|
519
|
+
assert inspect.isclass(cls), cls
|
520
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
521
|
+
if not auto_dict:
|
522
|
+
raise TypeError(
|
523
|
+
f'Cannot load class {type_name!r}.\n'
|
524
|
+
'Try pass `auto_dict=True` to load the object into a dict '
|
525
|
+
'without depending on the type.'
|
526
|
+
) from e
|
527
|
+
elif not auto_dict:
|
528
|
+
raise TypeError(
|
529
|
+
f'Type name \'{type_name}\' is not registered '
|
530
|
+
'with a `pg.JSONConvertible` subclass.\n'
|
531
|
+
'Try pass `auto_import=True` to load the type from its module, '
|
532
|
+
'or pass `auto_dict=True` to load the object into a dict '
|
533
|
+
'without depending on the type.'
|
534
|
+
)
|
535
|
+
|
536
|
+
factory_fn = getattr(cls, 'from_json', None)
|
537
|
+
if cls is not None and factory_fn is None and not auto_dict:
|
429
538
|
raise TypeError(
|
430
|
-
f'
|
431
|
-
|
432
|
-
|
433
|
-
|
539
|
+
f'{cls} is not a `pg.JSONConvertible` subclass.'
|
540
|
+
'Try pass `auto_dict=True` to load the object into a dict '
|
541
|
+
'without depending on the type.'
|
542
|
+
)
|
543
|
+
|
544
|
+
if factory_fn is None and auto_dict:
|
545
|
+
v['type_name'] = type_name
|
546
|
+
v.pop(JSONConvertible.TYPE_NAME_KEY)
|
547
|
+
return True
|
548
|
+
assert factory_fn is not None
|
434
549
|
|
550
|
+
v[JSONConvertible.TYPE_NAME_KEY] = factory_fn
|
551
|
+
return True
|
435
552
|
|
436
|
-
def
|
437
|
-
"""Inplace strips the "_type" key from a JSON tree."""
|
438
|
-
def _strip_type(v) -> None:
|
553
|
+
def _visit(v) -> None:
|
439
554
|
if isinstance(v, (tuple, list)):
|
440
555
|
for x in v:
|
441
|
-
|
556
|
+
_visit(x)
|
442
557
|
elif isinstance(v, dict):
|
443
|
-
if
|
444
|
-
|
445
|
-
|
446
|
-
|
558
|
+
if _resolve_typename(v):
|
559
|
+
# Only resolve children when _types in this tree is not resolved
|
560
|
+
# previously
|
561
|
+
for x in v.values():
|
562
|
+
_visit(x)
|
447
563
|
|
448
|
-
|
564
|
+
_visit(json_value)
|
449
565
|
return json_value
|
450
566
|
|
451
567
|
|
@@ -610,6 +726,9 @@ def _load_symbol(type_name: str) -> Any:
|
|
610
726
|
module_name = '.'.join(maybe_modules[:module_end_pos])
|
611
727
|
parent_symbols = maybe_modules[module_end_pos:]
|
612
728
|
|
729
|
+
if not module_name:
|
730
|
+
raise ModuleNotFoundError(f'Cannot load symbol {type_name!r}.')
|
731
|
+
|
613
732
|
# Import module and lookup parent symbols.
|
614
733
|
module = importlib.import_module(module_name)
|
615
734
|
parent = module
|
@@ -622,21 +741,26 @@ def _load_symbol(type_name: str) -> Any:
|
|
622
741
|
return symbol
|
623
742
|
|
624
743
|
|
625
|
-
def _type_from_json(json_value: Dict[str, str]) -> Type[Any]:
|
744
|
+
def _type_from_json(json_value: Dict[str, str], **kwargs) -> Type[Any]:
|
626
745
|
"""Loads a type from a JSON dict."""
|
746
|
+
del kwargs
|
627
747
|
t = _load_symbol(json_value['name'])
|
628
748
|
if 'args' in json_value:
|
629
|
-
return _bind_type_args(
|
749
|
+
return _bind_type_args(
|
750
|
+
t, from_json(json_value['args'], _typename_resolved=True)
|
751
|
+
)
|
630
752
|
return t
|
631
753
|
|
632
754
|
|
633
|
-
def _function_from_json(
|
755
|
+
def _function_from_json(
|
756
|
+
json_value: Dict[str, str], **kwargs) -> types.FunctionType:
|
634
757
|
"""Loads a function from a JSON dict."""
|
758
|
+
del kwargs
|
635
759
|
function_name = json_value['name']
|
636
760
|
if 'code' in json_value:
|
637
761
|
code = marshal.loads(
|
638
762
|
base64.decodebytes(json_value['code'].encode('utf-8')))
|
639
|
-
defaults = from_json(json_value['defaults'])
|
763
|
+
defaults = from_json(json_value['defaults'], _typename_resolved=True)
|
640
764
|
return types.FunctionType(
|
641
765
|
code=code,
|
642
766
|
globals=globals(),
|
@@ -646,8 +770,9 @@ def _function_from_json(json_value: Dict[str, str]) -> types.FunctionType:
|
|
646
770
|
return _load_symbol(function_name)
|
647
771
|
|
648
772
|
|
649
|
-
def _method_from_json(json_value: Dict[str, str]) -> types.MethodType:
|
773
|
+
def _method_from_json(json_value: Dict[str, str], **kwargs) -> types.MethodType:
|
650
774
|
"""Loads a class method from a JSON dict."""
|
775
|
+
del kwargs
|
651
776
|
return _load_symbol(json_value['name'])
|
652
777
|
|
653
778
|
|
@@ -11,13 +11,11 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
"""Tests for pyglove.object_utils.json_conversion."""
|
15
|
-
|
16
14
|
import abc
|
17
15
|
import typing
|
18
16
|
import unittest
|
19
|
-
from pyglove.core.object_utils import json_conversion
|
20
17
|
from pyglove.core.typing import inspect as pg_inspect
|
18
|
+
from pyglove.core.utils import json_conversion
|
21
19
|
|
22
20
|
|
23
21
|
class X:
|
@@ -96,7 +94,7 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
96
94
|
self.x = x
|
97
95
|
|
98
96
|
def to_json(self):
|
99
|
-
return
|
97
|
+
return self.__class__.to_json_dict({
|
100
98
|
'x': self.x
|
101
99
|
})
|
102
100
|
|
@@ -130,6 +128,27 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
130
128
|
(typename(B), C),
|
131
129
|
list(json_conversion.JSONConvertible.registered_types()))
|
132
130
|
|
131
|
+
# Test load_types_for_deserialization.
|
132
|
+
class D(C):
|
133
|
+
auto_register = False
|
134
|
+
|
135
|
+
with self.assertRaisesRegex(TypeError, 'Cannot load class .*'):
|
136
|
+
json_conversion.from_json(D(1).to_json())
|
137
|
+
|
138
|
+
self.assertEqual(
|
139
|
+
json_conversion.JSONConvertible._TYPE_REGISTRY._ondemand_registry_stack,
|
140
|
+
[]
|
141
|
+
)
|
142
|
+
with json_conversion.JSONConvertible.load_types_for_deserialization(A):
|
143
|
+
with json_conversion.JSONConvertible.load_types_for_deserialization(
|
144
|
+
D) as ondemand_registry:
|
145
|
+
self.assertEqual(ondemand_registry, {'A': A, 'D': D})
|
146
|
+
self.assertIsNotNone(json_conversion.from_json(D(1).to_json()))
|
147
|
+
self.assertEqual(
|
148
|
+
json_conversion.JSONConvertible._TYPE_REGISTRY._ondemand_registry_stack,
|
149
|
+
[]
|
150
|
+
)
|
151
|
+
|
133
152
|
def test_json_conversion(self):
|
134
153
|
|
135
154
|
class T(json_conversion.JSONConvertible):
|
@@ -171,21 +190,41 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
171
190
|
T()
|
172
191
|
)
|
173
192
|
|
174
|
-
# Test bad cases.
|
175
|
-
with self.assertRaisesRegex(
|
176
|
-
ValueError, 'Tuple should have at least one element besides .*'):
|
177
|
-
json_conversion.from_json(['__tuple__'])
|
178
|
-
|
179
|
-
with self.assertRaisesRegex(
|
180
|
-
TypeError, 'Type name .* is not registered'):
|
181
|
-
json_conversion.from_json({'_type': '__main__.ABC'})
|
182
|
-
|
183
193
|
def assert_conversion_is(self, v):
|
184
194
|
self.assertIs(json_conversion.from_json(json_conversion.to_json(v)), v)
|
185
195
|
|
186
196
|
def assert_conversion_equal(self, v):
|
187
197
|
self.assertEqual(json_conversion.from_json(json_conversion.to_json(v)), v)
|
188
198
|
|
199
|
+
class CustomJsonConvertible(json_conversion.JSONConvertible):
|
200
|
+
auto_register = False
|
201
|
+
|
202
|
+
def __init__(self, x=None):
|
203
|
+
self.x = x
|
204
|
+
|
205
|
+
def to_json(self):
|
206
|
+
return self.to_json_dict(
|
207
|
+
dict(x=(self.x, None)), exclude_default=True
|
208
|
+
)
|
209
|
+
|
210
|
+
def __eq__(self, other):
|
211
|
+
return isinstance(other, self.__class__) and self.x == other.x
|
212
|
+
|
213
|
+
def __ne__(self, other):
|
214
|
+
return not self.__eq__(other)
|
215
|
+
|
216
|
+
def test_json_conversion_with_auto_import(self):
|
217
|
+
json_dict = json_conversion.to_json(self.CustomJsonConvertible(1))
|
218
|
+
|
219
|
+
with self.assertRaisesRegex(
|
220
|
+
TypeError, 'Type name .* is not registered'):
|
221
|
+
json_conversion.from_json(json_dict, auto_import=False)
|
222
|
+
|
223
|
+
self.assertEqual(
|
224
|
+
json_conversion.from_json(json_dict),
|
225
|
+
self.CustomJsonConvertible(1)
|
226
|
+
)
|
227
|
+
|
189
228
|
def test_json_conversion_for_types(self):
|
190
229
|
# Built-in types.
|
191
230
|
self.assert_conversion_is(int)
|
@@ -297,7 +336,8 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
297
336
|
ValueError, 'Cannot decode opaque object with pickle.'):
|
298
337
|
json_conversion.from_json(json_dict)
|
299
338
|
|
300
|
-
def
|
339
|
+
def test_json_conversion_auto_dict(self):
|
340
|
+
# Does not exist.
|
301
341
|
self.assertEqual(
|
302
342
|
json_conversion.from_json([
|
303
343
|
'__tuple__',
|
@@ -306,12 +346,53 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
306
346
|
'_type': 'Unknown type',
|
307
347
|
'x': [{
|
308
348
|
'_type': 'Unknown type',
|
349
|
+
}, {
|
350
|
+
'_type': 'function',
|
351
|
+
'name': 'builtins.print'
|
309
352
|
}]
|
310
353
|
}
|
311
|
-
],
|
312
|
-
(1, {
|
354
|
+
], auto_dict=True),
|
355
|
+
(1, {
|
356
|
+
'type_name': 'Unknown type',
|
357
|
+
'x': [{
|
358
|
+
'type_name': 'Unknown type',
|
359
|
+
}, print]
|
360
|
+
})
|
313
361
|
)
|
314
362
|
|
363
|
+
def test_json_conversion_with_bad_types(self):
|
364
|
+
# Bad tuple.
|
365
|
+
with self.assertRaisesRegex(
|
366
|
+
ValueError, 'Tuple should have at least one element besides .*'):
|
367
|
+
json_conversion.from_json(['__tuple__'])
|
368
|
+
|
369
|
+
# Unregistered type without auto_import.
|
370
|
+
with self.assertRaisesRegex(
|
371
|
+
TypeError, 'Type name .* is not registered with'
|
372
|
+
):
|
373
|
+
json_conversion.from_json(
|
374
|
+
{
|
375
|
+
'_type': 'Unknown type',
|
376
|
+
'x': [{
|
377
|
+
'_type': 'Unknown type',
|
378
|
+
}]
|
379
|
+
}, auto_import=False
|
380
|
+
)
|
381
|
+
|
382
|
+
# Type does not exist.
|
383
|
+
with self.assertRaisesRegex(
|
384
|
+
TypeError, 'Cannot load class .*'):
|
385
|
+
json_conversion.from_json({'_type': '__main__.ABC'})
|
386
|
+
|
387
|
+
# Type exist but not a JSONConvertible subclass.
|
388
|
+
class A:
|
389
|
+
pass
|
390
|
+
|
391
|
+
json_conversion.JSONConvertible.register('__main__.A', A)
|
392
|
+
with self.assertRaisesRegex(
|
393
|
+
TypeError, '.* is not a `pg.JSONConvertible` subclass'):
|
394
|
+
json_conversion.from_json({'_type': '__main__.A'})
|
395
|
+
|
315
396
|
|
316
397
|
if __name__ == '__main__':
|
317
398
|
unittest.main()
|
@@ -14,11 +14,11 @@
|
|
14
14
|
"""Representing missing value for a field."""
|
15
15
|
|
16
16
|
from typing import Any, Dict
|
17
|
-
from pyglove.core.
|
18
|
-
from pyglove.core.
|
17
|
+
from pyglove.core.utils import formatting
|
18
|
+
from pyglove.core.utils import json_conversion
|
19
19
|
|
20
20
|
|
21
|
-
class MissingValue(
|
21
|
+
class MissingValue(formatting.Formattable, json_conversion.JSONConvertible):
|
22
22
|
"""Value placeholder for an unassigned attribute."""
|
23
23
|
|
24
24
|
def format(self, *args, **kwargs): # pytype: disable=signature-mismatch
|
@@ -11,11 +11,9 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
"""Tests for pyglove.object_utils.missing."""
|
15
|
-
|
16
14
|
import unittest
|
17
|
-
from pyglove.core.
|
18
|
-
from pyglove.core.
|
15
|
+
from pyglove.core.utils import json_conversion
|
16
|
+
from pyglove.core.utils import missing
|
19
17
|
|
20
18
|
|
21
19
|
class MissingValueTest(unittest.TestCase):
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# Copyright 2025 The PyGlove Authors
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
"""Utility library for text coloring."""
|
15
|
+
|
16
|
+
import re
|
17
|
+
from typing import List, Optional
|
18
|
+
|
19
|
+
try:
|
20
|
+
import termcolor # pylint: disable=g-import-not-at-top
|
21
|
+
except ImportError:
|
22
|
+
termcolor = None
|
23
|
+
|
24
|
+
|
25
|
+
# Regular expression for ANSI color characters.
|
26
|
+
_ANSI_COLOR_REGEX = re.compile(r'\x1b\[[0-9;]*m')
|
27
|
+
|
28
|
+
|
29
|
+
def decolor(text: str) -> str:
|
30
|
+
"""De-colors a string that may contains ANSI color characters."""
|
31
|
+
return re.sub(_ANSI_COLOR_REGEX, '', text)
|
32
|
+
|
33
|
+
|
34
|
+
def colored(
|
35
|
+
text: str,
|
36
|
+
color: Optional[str] = None,
|
37
|
+
background: Optional[str] = None,
|
38
|
+
styles: Optional[List[str]] = None,
|
39
|
+
) -> str:
|
40
|
+
"""Returns the colored text with ANSI color characters.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
text: A string that may or may not already has ANSI color characters.
|
44
|
+
color: A string for text colors. Applicable values are:
|
45
|
+
'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
|
46
|
+
background: A string for background colors. Applicable values are:
|
47
|
+
'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
|
48
|
+
styles: A list of strings for applying styles on the text.
|
49
|
+
Applicable values are:
|
50
|
+
'bold', 'dark', 'underline', 'blink', 'reverse', 'concealed'.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
A string with ANSI color characters embracing the entire text.
|
54
|
+
"""
|
55
|
+
if not termcolor:
|
56
|
+
return text
|
57
|
+
return termcolor.colored(
|
58
|
+
text,
|
59
|
+
color=color,
|
60
|
+
on_color=('on_' + background) if background else None,
|
61
|
+
attrs=styles
|
62
|
+
)
|
63
|
+
|
64
|
+
|
65
|
+
def colored_block(
|
66
|
+
text: str,
|
67
|
+
block_start: str,
|
68
|
+
block_end: str,
|
69
|
+
color: Optional[str] = None,
|
70
|
+
background: Optional[str] = None,
|
71
|
+
styles: Optional[List[str]] = None,
|
72
|
+
) -> str:
|
73
|
+
"""Apply colors to text blocks.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
text: A string that may or may not already has ANSI color characters.
|
77
|
+
block_start: A string that signals the start of a block. E.g. '{{'
|
78
|
+
block_end: A string that signals the end of a block. E.g. '}}'.
|
79
|
+
color: A string for text colors. Applicable values are:
|
80
|
+
'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
|
81
|
+
background: A string for background colors. Applicable values are:
|
82
|
+
'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'.
|
83
|
+
styles: A list of strings for applying styles on the text.
|
84
|
+
Applicable values are:
|
85
|
+
'bold', 'dark', 'underline', 'blink', 'reverse', 'concealed'.
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
A string with ANSI color characters embracing the matched text blocks.
|
89
|
+
"""
|
90
|
+
if not color and not background and not styles:
|
91
|
+
return text
|
92
|
+
|
93
|
+
s = []
|
94
|
+
start_index = 0
|
95
|
+
end_index = 0
|
96
|
+
previous_color = None
|
97
|
+
|
98
|
+
def write_nonblock_text(text: str, previous_color: Optional[str]):
|
99
|
+
if previous_color:
|
100
|
+
s.append(previous_color)
|
101
|
+
s.append(text)
|
102
|
+
|
103
|
+
while start_index < len(text):
|
104
|
+
start_index = text.find(block_start, end_index)
|
105
|
+
if start_index == -1:
|
106
|
+
write_nonblock_text(text[end_index:], previous_color)
|
107
|
+
break
|
108
|
+
|
109
|
+
# Deal with text since last block.
|
110
|
+
since_last_block = text[end_index:start_index]
|
111
|
+
write_nonblock_text(since_last_block, previous_color)
|
112
|
+
colors = re.findall(_ANSI_COLOR_REGEX, since_last_block)
|
113
|
+
if colors:
|
114
|
+
previous_color = colors[-1]
|
115
|
+
|
116
|
+
# Match block.
|
117
|
+
end_index = text.find(block_end, start_index + len(block_start))
|
118
|
+
if end_index == -1:
|
119
|
+
write_nonblock_text(text[start_index:], previous_color)
|
120
|
+
break
|
121
|
+
end_index += len(block_end)
|
122
|
+
|
123
|
+
# Write block text.
|
124
|
+
block = text[start_index:end_index]
|
125
|
+
block = colored(
|
126
|
+
block, color=color, background=background, styles=styles)
|
127
|
+
s.append(block)
|
128
|
+
return ''.join(s)
|