pyglove 0.5.0.dev202510230131__py3-none-any.whl → 0.5.0.dev202511300809__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/io/file_system.py +295 -2
- pyglove/core/io/file_system_test.py +291 -0
- pyglove/core/symbolic/__init__.py +7 -0
- pyglove/core/symbolic/base.py +24 -9
- pyglove/core/symbolic/list_test.py +1 -2
- pyglove/core/symbolic/object.py +2 -1
- pyglove/core/symbolic/object_test.py +13 -10
- pyglove/core/symbolic/unknown_symbols.py +147 -0
- pyglove/core/symbolic/unknown_symbols_test.py +100 -0
- pyglove/core/typing/annotation_conversion.py +3 -0
- pyglove/core/typing/annotation_conversion_test.py +11 -19
- pyglove/core/typing/type_conversion.py +17 -3
- pyglove/core/typing/type_conversion_test.py +7 -2
- pyglove/core/typing/value_specs.py +5 -1
- pyglove/core/typing/value_specs_test.py +5 -0
- pyglove/core/utils/json_conversion.py +107 -49
- pyglove/core/utils/json_conversion_test.py +85 -10
- pyglove/core/views/html/controls/tab.py +33 -0
- pyglove/core/views/html/controls/tab_test.py +37 -0
- pyglove/ext/evolution/base_test.py +1 -1
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/METADATA +8 -1
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/RECORD +25 -23
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/WHEEL +0 -0
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/licenses/LICENSE +0 -0
- {pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/top_level.txt +0 -0
|
@@ -685,7 +685,7 @@ def from_json(
|
|
|
685
685
|
*,
|
|
686
686
|
context: Optional[JSONConversionContext] = None,
|
|
687
687
|
auto_import: bool = True,
|
|
688
|
-
|
|
688
|
+
convert_unknown: bool = False,
|
|
689
689
|
**kwargs
|
|
690
690
|
) -> Any:
|
|
691
691
|
"""Deserializes a (maybe) JSONConvertible value from JSON value.
|
|
@@ -697,8 +697,13 @@ def from_json(
|
|
|
697
697
|
identify its parent module and automatically import it. For example,
|
|
698
698
|
if the type is 'foo.bar.A', PyGlove will try to import 'foo.bar' and
|
|
699
699
|
find the class 'A' within the imported module.
|
|
700
|
-
|
|
701
|
-
|
|
700
|
+
convert_unknown: If True, when a '_type' is not registered and cannot
|
|
701
|
+
be imported, PyGlove will create objects of:
|
|
702
|
+
- `pg.symbolic.UnknownType` for unknown types;
|
|
703
|
+
- `pg.symbolic.UnknownTypedObject` for objects of unknown types;
|
|
704
|
+
- `pg.symbolic.UnknownFunction` for unknown functions;
|
|
705
|
+
- `pg.symbolic.UnknownMethod` for unknown methods.
|
|
706
|
+
If False, TypeError will be raised.
|
|
702
707
|
**kwargs: Keyword arguments that will be passed to JSONConvertible.__init__.
|
|
703
708
|
|
|
704
709
|
Returns:
|
|
@@ -707,14 +712,21 @@ def from_json(
|
|
|
707
712
|
if context is None:
|
|
708
713
|
if (isinstance(json_value, dict)
|
|
709
714
|
and (context_node := json_value.get(JSONConvertible.CONTEXT_KEY))):
|
|
710
|
-
context = JSONConversionContext.from_json(
|
|
715
|
+
context = JSONConversionContext.from_json(
|
|
716
|
+
context_node,
|
|
717
|
+
auto_import=auto_import,
|
|
718
|
+
convert_unknown=convert_unknown,
|
|
719
|
+
**kwargs
|
|
720
|
+
)
|
|
711
721
|
json_value = json_value[JSONConvertible.ROOT_VALUE_KEY]
|
|
712
722
|
else:
|
|
713
723
|
context = JSONConversionContext()
|
|
714
724
|
|
|
715
725
|
typename_resolved = kwargs.pop('_typename_resolved', False)
|
|
716
726
|
if not typename_resolved:
|
|
717
|
-
json_value = resolve_typenames(
|
|
727
|
+
json_value = resolve_typenames(
|
|
728
|
+
json_value, auto_import=auto_import, convert_unknown=convert_unknown
|
|
729
|
+
)
|
|
718
730
|
|
|
719
731
|
def child_from(v):
|
|
720
732
|
return from_json(v, context=context, _typename_resolved=True, **kwargs)
|
|
@@ -743,7 +755,7 @@ def from_json(
|
|
|
743
755
|
def resolve_typenames(
|
|
744
756
|
json_value: JSONValueType,
|
|
745
757
|
auto_import: bool = True,
|
|
746
|
-
|
|
758
|
+
convert_unknown: bool = False,
|
|
747
759
|
) -> JSONValueType:
|
|
748
760
|
"""Inplace resolves the "_type" keys with their factories in a JSON tree."""
|
|
749
761
|
|
|
@@ -755,11 +767,11 @@ def resolve_typenames(
|
|
|
755
767
|
return False
|
|
756
768
|
type_name = v[JSONConvertible.TYPE_NAME_KEY]
|
|
757
769
|
if type_name == 'type':
|
|
758
|
-
factory_fn = _type_from_json
|
|
770
|
+
factory_fn = _type_from_json(convert_unknown)
|
|
759
771
|
elif type_name == 'function':
|
|
760
|
-
factory_fn = _function_from_json
|
|
772
|
+
factory_fn = _function_from_json(convert_unknown)
|
|
761
773
|
elif type_name == 'method':
|
|
762
|
-
factory_fn = _method_from_json
|
|
774
|
+
factory_fn = _method_from_json(convert_unknown)
|
|
763
775
|
else:
|
|
764
776
|
cls = JSONConvertible.class_from_typename(type_name)
|
|
765
777
|
if cls is None:
|
|
@@ -768,32 +780,38 @@ def resolve_typenames(
|
|
|
768
780
|
cls = _load_symbol(type_name)
|
|
769
781
|
assert inspect.isclass(cls), cls
|
|
770
782
|
except (ModuleNotFoundError, AttributeError) as e:
|
|
771
|
-
if not
|
|
783
|
+
if not convert_unknown:
|
|
772
784
|
raise TypeError(
|
|
773
785
|
f'Cannot load class {type_name!r}.\n'
|
|
774
|
-
'Try pass `
|
|
775
|
-
'without depending on the type.'
|
|
786
|
+
'Try pass `convert_unknown=True` to load the object into '
|
|
787
|
+
'`pg.symbolic.UnknownObject` without depending on the type.'
|
|
776
788
|
) from e
|
|
777
|
-
elif not
|
|
789
|
+
elif not convert_unknown:
|
|
778
790
|
raise TypeError(
|
|
779
791
|
f'Type name \'{type_name}\' is not registered '
|
|
780
792
|
'with a `pg.JSONConvertible` subclass.\n'
|
|
781
|
-
'Try pass `auto_import=True` to load the type from its module
|
|
782
|
-
'or pass `auto_dict=True` to load the object into a dict '
|
|
783
|
-
'without depending on the type.'
|
|
793
|
+
'Try pass `auto_import=True` to load the type from its module.'
|
|
784
794
|
)
|
|
785
795
|
|
|
786
796
|
factory_fn = getattr(cls, 'from_json', None)
|
|
787
|
-
if cls is not None and factory_fn is None and not
|
|
797
|
+
if cls is not None and factory_fn is None and not convert_unknown:
|
|
788
798
|
raise TypeError(
|
|
789
799
|
f'{cls} is not a `pg.JSONConvertible` subclass.'
|
|
790
|
-
'Try pass `
|
|
791
|
-
'without depending on the type.'
|
|
800
|
+
'Try pass `convert_unknown=True` to load the object into a '
|
|
801
|
+
'`pg.symbolic.UnknownObject` without depending on the type.'
|
|
792
802
|
)
|
|
793
803
|
|
|
794
|
-
if factory_fn is None and
|
|
795
|
-
|
|
796
|
-
|
|
804
|
+
if factory_fn is None and convert_unknown:
|
|
805
|
+
type_name = v[JSONConvertible.TYPE_NAME_KEY]
|
|
806
|
+
def _factory_fn(json_value: Dict[str, Any], **kwargs):
|
|
807
|
+
del kwargs
|
|
808
|
+
# See `pg.symbolic.UnknownObject` for details.
|
|
809
|
+
unknown_object_cls = JSONConvertible.class_from_typename(
|
|
810
|
+
'unknown_object'
|
|
811
|
+
)
|
|
812
|
+
return unknown_object_cls(type_name=type_name, **json_value) # pytype: disable=wrong-keyword-args
|
|
813
|
+
|
|
814
|
+
v[JSONConvertible.TYPE_NAME_KEY] = _factory_fn
|
|
797
815
|
return True
|
|
798
816
|
assert factory_fn is not None
|
|
799
817
|
|
|
@@ -992,39 +1010,79 @@ def _load_symbol(type_name: str) -> Any:
|
|
|
992
1010
|
return symbol
|
|
993
1011
|
|
|
994
1012
|
|
|
995
|
-
def _type_from_json(
|
|
1013
|
+
def _type_from_json(convert_unknown: bool) -> Callable[..., Any]:
|
|
996
1014
|
"""Loads a type from a JSON dict."""
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1015
|
+
def _fn(json_value: Dict[str, str], **kwargs) -> Type[Any]:
|
|
1016
|
+
del kwargs
|
|
1017
|
+
try:
|
|
1018
|
+
t = _load_symbol(json_value['name'])
|
|
1019
|
+
if 'args' in json_value:
|
|
1020
|
+
return _bind_type_args(
|
|
1021
|
+
t, from_json(json_value['args'], _typename_resolved=True)
|
|
1022
|
+
)
|
|
1023
|
+
return t
|
|
1024
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
|
1025
|
+
if not convert_unknown:
|
|
1026
|
+
raise TypeError(
|
|
1027
|
+
f'Cannot load type {json_value["name"]!r}.\n'
|
|
1028
|
+
'Try pass `convert_unknown=True` to load the object '
|
|
1029
|
+
'into `pg.UnknownType` without depending on the type.'
|
|
1030
|
+
) from e
|
|
1031
|
+
# See `pg.symbolic.UnknownType` for details.
|
|
1032
|
+
json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_type'
|
|
1033
|
+
return from_json(json_value)
|
|
1034
|
+
return _fn
|
|
1004
1035
|
|
|
1005
1036
|
|
|
1006
1037
|
def _function_from_json(
|
|
1007
|
-
|
|
1038
|
+
convert_unknown: bool
|
|
1039
|
+
) -> Callable[..., types.FunctionType]:
|
|
1008
1040
|
"""Loads a function from a JSON dict."""
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
code
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1041
|
+
def _fn(json_value: Dict[str, str], **kwargs) -> types.FunctionType:
|
|
1042
|
+
del kwargs
|
|
1043
|
+
function_name = json_value['name']
|
|
1044
|
+
if 'code' in json_value:
|
|
1045
|
+
code = marshal.loads(
|
|
1046
|
+
base64.decodebytes(json_value['code'].encode('utf-8')))
|
|
1047
|
+
defaults = from_json(json_value['defaults'], _typename_resolved=True)
|
|
1048
|
+
return types.FunctionType(
|
|
1049
|
+
code=code,
|
|
1050
|
+
globals=globals(),
|
|
1051
|
+
argdefs=defaults,
|
|
1052
|
+
)
|
|
1053
|
+
else:
|
|
1054
|
+
try:
|
|
1055
|
+
return _load_symbol(function_name)
|
|
1056
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
|
1057
|
+
if not convert_unknown:
|
|
1058
|
+
raise TypeError(
|
|
1059
|
+
f'Cannot load function {function_name!r}.\n'
|
|
1060
|
+
'Try pass `convert_unknown=True` to load the object into '
|
|
1061
|
+
'`pg.UnknownFunction` without depending on the type.'
|
|
1062
|
+
) from e
|
|
1063
|
+
json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_function'
|
|
1064
|
+
return from_json(json_value)
|
|
1065
|
+
return _fn
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _method_from_json(
|
|
1069
|
+
convert_unknown: bool
|
|
1070
|
+
) -> Callable[..., types.MethodType]:
|
|
1025
1071
|
"""Loads a class method from a JSON dict."""
|
|
1026
|
-
|
|
1027
|
-
|
|
1072
|
+
def _fn(json_value: Dict[str, str], **kwargs) -> types.MethodType:
|
|
1073
|
+
del kwargs
|
|
1074
|
+
try:
|
|
1075
|
+
return _load_symbol(json_value['name'])
|
|
1076
|
+
except (ModuleNotFoundError, AttributeError) as e:
|
|
1077
|
+
if not convert_unknown:
|
|
1078
|
+
raise TypeError(
|
|
1079
|
+
f'Cannot load method {json_value["name"]!r}.\n'
|
|
1080
|
+
'Try pass `convert_unknown=True` to load the object '
|
|
1081
|
+
'into `pg.UnknownMethod` without depending on the type.'
|
|
1082
|
+
) from e
|
|
1083
|
+
json_value[JSONConvertible.TYPE_NAME_KEY] = 'unknown_method'
|
|
1084
|
+
return from_json(json_value)
|
|
1085
|
+
return _fn
|
|
1028
1086
|
|
|
1029
1087
|
|
|
1030
1088
|
def _bind_type_args(t, args):
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import abc
|
|
15
15
|
import typing
|
|
16
16
|
import unittest
|
|
17
|
+
from pyglove.core.symbolic import unknown_symbols
|
|
17
18
|
from pyglove.core.typing import inspect as pg_inspect
|
|
18
19
|
from pyglove.core.utils import json_conversion
|
|
19
20
|
|
|
@@ -271,7 +272,8 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
|
271
272
|
self.assert_conversion_is(typing.List[typing.List[int]])
|
|
272
273
|
self.assert_conversion_is(typing.Annotated[int, 'abc'])
|
|
273
274
|
self.assert_conversion_is(typing.Dict[str, typing.Any])
|
|
274
|
-
|
|
275
|
+
# From Python 3.14, union no longer preserves `is` identity.
|
|
276
|
+
self.assert_conversion_equal(typing.Union[int, str])
|
|
275
277
|
self.assert_conversion_is(typing.Sequence[int])
|
|
276
278
|
self.assert_conversion_is(typing.Set[int])
|
|
277
279
|
self.assert_conversion_is(typing.FrozenSet[int])
|
|
@@ -307,6 +309,19 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
|
307
309
|
self.assertEqual(baz1(1), 2)
|
|
308
310
|
self.assertEqual(baz1(1, 2), 3)
|
|
309
311
|
|
|
312
|
+
with self.assertRaisesRegex(
|
|
313
|
+
TypeError, 'Cannot load function .*'):
|
|
314
|
+
json_conversion.from_json(
|
|
315
|
+
{'_type': 'function', 'name': 'non_existent_function'}
|
|
316
|
+
)
|
|
317
|
+
self.assertEqual(
|
|
318
|
+
json_conversion.from_json(
|
|
319
|
+
{'_type': 'function', 'name': 'non_existent_function'},
|
|
320
|
+
convert_unknown=True
|
|
321
|
+
),
|
|
322
|
+
unknown_symbols.UnknownFunction('non_existent_function')
|
|
323
|
+
)
|
|
324
|
+
|
|
310
325
|
def test_json_conversion_for_methods(self):
|
|
311
326
|
# Test class-level method.
|
|
312
327
|
f = json_conversion.from_json(json_conversion.to_json(X.Y.Z.class_method))
|
|
@@ -319,6 +334,19 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
|
319
334
|
ValueError, 'Cannot convert instance method .* to JSON.'):
|
|
320
335
|
json_conversion.to_json(X.Y.Z().instance_method)
|
|
321
336
|
|
|
337
|
+
with self.assertRaisesRegex(
|
|
338
|
+
TypeError, 'Cannot load method .*'):
|
|
339
|
+
json_conversion.from_json(
|
|
340
|
+
{'_type': 'method', 'name': 'non_existent_method'}
|
|
341
|
+
)
|
|
342
|
+
self.assertEqual(
|
|
343
|
+
json_conversion.from_json(
|
|
344
|
+
{'_type': 'method', 'name': 'non_existent_method'},
|
|
345
|
+
convert_unknown=True
|
|
346
|
+
),
|
|
347
|
+
unknown_symbols.UnknownMethod('non_existent_method')
|
|
348
|
+
)
|
|
349
|
+
|
|
322
350
|
def test_json_conversion_for_opaque_objects(self):
|
|
323
351
|
self.assert_conversion_equal(X(1))
|
|
324
352
|
|
|
@@ -335,12 +363,15 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
|
335
363
|
ValueError, 'Cannot decode opaque object with pickle.'):
|
|
336
364
|
json_conversion.from_json(json_dict)
|
|
337
365
|
|
|
338
|
-
def
|
|
339
|
-
# Does not exist.
|
|
366
|
+
def test_json_conversion_convert_unknown(self):
|
|
340
367
|
self.assertEqual(
|
|
341
368
|
json_conversion.from_json([
|
|
342
369
|
'__tuple__',
|
|
343
370
|
1,
|
|
371
|
+
{
|
|
372
|
+
'_type': 'type',
|
|
373
|
+
'name': 'Unknown type',
|
|
374
|
+
},
|
|
344
375
|
{
|
|
345
376
|
'_type': 'Unknown type',
|
|
346
377
|
'x': [{
|
|
@@ -350,13 +381,18 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
|
350
381
|
'name': 'builtins.print'
|
|
351
382
|
}]
|
|
352
383
|
}
|
|
353
|
-
],
|
|
354
|
-
(
|
|
355
|
-
|
|
356
|
-
'
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
384
|
+
], convert_unknown=True),
|
|
385
|
+
(
|
|
386
|
+
1,
|
|
387
|
+
unknown_symbols.UnknownType('Unknown type'),
|
|
388
|
+
unknown_symbols.UnknownTypedObject(
|
|
389
|
+
type_name='Unknown type',
|
|
390
|
+
x=[
|
|
391
|
+
unknown_symbols.UnknownTypedObject('Unknown type'),
|
|
392
|
+
print
|
|
393
|
+
]
|
|
394
|
+
)
|
|
395
|
+
)
|
|
360
396
|
)
|
|
361
397
|
|
|
362
398
|
def test_json_conversion_with_bad_types(self):
|
|
@@ -383,6 +419,10 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
|
383
419
|
TypeError, 'Cannot load class .*'):
|
|
384
420
|
json_conversion.from_json({'_type': '__main__.ABC'})
|
|
385
421
|
|
|
422
|
+
with self.assertRaisesRegex(
|
|
423
|
+
TypeError, 'Cannot load type .*'):
|
|
424
|
+
json_conversion.from_json({'_type': 'type', 'name': '__main__.ABC'})
|
|
425
|
+
|
|
386
426
|
# Type exist but not a JSONConvertible subclass.
|
|
387
427
|
class A:
|
|
388
428
|
pass
|
|
@@ -451,6 +491,41 @@ class JSONConvertibleTest(unittest.TestCase):
|
|
|
451
491
|
self.assertIs(y_prime['t'], y_prime['v'][1])
|
|
452
492
|
self.assertIs(y_prime['u'], y_prime['v'][0])
|
|
453
493
|
|
|
494
|
+
def test_json_conversion_with_sharing_convert_unknown(self):
|
|
495
|
+
self.assertEqual(
|
|
496
|
+
json_conversion.from_json(
|
|
497
|
+
{
|
|
498
|
+
'__context__': {
|
|
499
|
+
'shared_objects': [
|
|
500
|
+
{
|
|
501
|
+
'_type': 'type',
|
|
502
|
+
'name': '__main__.ABC',
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
'_type': '__main__.ABC',
|
|
506
|
+
'x': 1
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
},
|
|
510
|
+
'__root__': [
|
|
511
|
+
{
|
|
512
|
+
'__ref__': 0
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
'__ref__': 1
|
|
516
|
+
},
|
|
517
|
+
]
|
|
518
|
+
},
|
|
519
|
+
convert_unknown=True
|
|
520
|
+
),
|
|
521
|
+
[
|
|
522
|
+
unknown_symbols.UnknownType('__main__.ABC'),
|
|
523
|
+
unknown_symbols.UnknownTypedObject(
|
|
524
|
+
type_name='__main__.ABC',
|
|
525
|
+
x=1
|
|
526
|
+
)
|
|
527
|
+
]
|
|
528
|
+
)
|
|
454
529
|
|
|
455
530
|
if __name__ == '__main__':
|
|
456
531
|
unittest.main()
|
|
@@ -158,6 +158,39 @@ class TabControl(HtmlControl):
|
|
|
158
158
|
position='beforebegin',
|
|
159
159
|
)
|
|
160
160
|
|
|
161
|
+
def remove(self, index_or_name: Union[int, str]) -> Tab:
|
|
162
|
+
"""Removes a tab identified by index or name."""
|
|
163
|
+
index = self.indexof(index_or_name)
|
|
164
|
+
if index == -1:
|
|
165
|
+
raise ValueError(f'Tab not found: {index_or_name!r}')
|
|
166
|
+
|
|
167
|
+
with pg_flags.notify_on_change(False):
|
|
168
|
+
tab = self.tabs.pop(index)
|
|
169
|
+
|
|
170
|
+
self._run_javascript(
|
|
171
|
+
f"""
|
|
172
|
+
const button = document.querySelectorAll('#{self.element_id()}-button-group > .tab-button')[{index}];
|
|
173
|
+
if (button) {{
|
|
174
|
+
button.remove();
|
|
175
|
+
}}
|
|
176
|
+
const content = document.querySelectorAll('#{self.element_id()}-content-group > .tab-content')[{index}];
|
|
177
|
+
if (content) {{
|
|
178
|
+
content.remove();
|
|
179
|
+
}}
|
|
180
|
+
"""
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if not self.tabs:
|
|
184
|
+
self._sync_members(selected=0)
|
|
185
|
+
return tab
|
|
186
|
+
|
|
187
|
+
if self.selected == index:
|
|
188
|
+
new_selected = index - 1 if index == len(self.tabs) else index
|
|
189
|
+
self.select(max(0, new_selected))
|
|
190
|
+
elif self.selected > index:
|
|
191
|
+
self._sync_members(selected=self.selected - 1)
|
|
192
|
+
return tab
|
|
193
|
+
|
|
161
194
|
def indexof(self, index_or_name: Union[int, str]) -> int:
|
|
162
195
|
if isinstance(index_or_name, int):
|
|
163
196
|
index = index_or_name
|
|
@@ -154,6 +154,43 @@ class TabControlTest(unittest.TestCase):
|
|
|
154
154
|
self.assertEqual(len(scripts), 1)
|
|
155
155
|
self.assertEqual(tab.selected, 1)
|
|
156
156
|
|
|
157
|
+
def test_remove(self):
|
|
158
|
+
tabs = [tab_lib.Tab(l, l, name=l) for l in ['a', 'b', 'c', 'd']]
|
|
159
|
+
tc = tab_lib.TabControl(tabs, selected='c')
|
|
160
|
+
self.assertEqual([t.name for t in tc.tabs], ['a', 'b', 'c', 'd'])
|
|
161
|
+
self.assertEqual(tc.selected, 2)
|
|
162
|
+
|
|
163
|
+
# Trigger rendering so scripts are tracked.
|
|
164
|
+
_ = tc.to_html()
|
|
165
|
+
with tc.track_scripts() as scripts:
|
|
166
|
+
tc.remove('b')
|
|
167
|
+
self.assertEqual(len(scripts), 1)
|
|
168
|
+
self.assertEqual([t.name for t in tc.tabs], ['a', 'c', 'd'])
|
|
169
|
+
self.assertEqual(tc.selected, 1)
|
|
170
|
+
|
|
171
|
+
with tc.track_scripts() as scripts:
|
|
172
|
+
# Remove currently selected tab 'c'
|
|
173
|
+
tc.remove('c')
|
|
174
|
+
# 1 script for remove, 1 for select.
|
|
175
|
+
self.assertEqual(len(scripts), 2)
|
|
176
|
+
self.assertEqual([t.name for t in tc.tabs], ['a', 'd'])
|
|
177
|
+
self.assertEqual(tc.selected, 1)
|
|
178
|
+
|
|
179
|
+
with tc.track_scripts() as scripts:
|
|
180
|
+
tc.remove(1)
|
|
181
|
+
self.assertEqual(len(scripts), 2)
|
|
182
|
+
self.assertEqual([t.name for t in tc.tabs], ['a'])
|
|
183
|
+
self.assertEqual(tc.selected, 0)
|
|
184
|
+
|
|
185
|
+
with tc.track_scripts() as scripts:
|
|
186
|
+
tc.remove('a')
|
|
187
|
+
self.assertEqual(len(scripts), 1)
|
|
188
|
+
self.assertEqual(tc.tabs, [])
|
|
189
|
+
self.assertEqual(tc.selected, 0)
|
|
190
|
+
|
|
191
|
+
with self.assertRaisesRegex(ValueError, 'Tab not found'):
|
|
192
|
+
tc.remove('x')
|
|
193
|
+
|
|
157
194
|
|
|
158
195
|
if __name__ == '__main__':
|
|
159
196
|
unittest.main()
|
{pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyglove
|
|
3
|
-
Version: 0.5.0.
|
|
3
|
+
Version: 0.5.0.dev202511300809
|
|
4
4
|
Summary: PyGlove: A library for manipulating Python objects.
|
|
5
5
|
Home-page: https://github.com/google/pyglove
|
|
6
6
|
Author: PyGlove Authors
|
|
@@ -23,6 +23,12 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: docstring-parser>=0.12
|
|
25
25
|
Requires-Dist: termcolor>=1.1.0
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: docstring-parser>=0.12; extra == "all"
|
|
28
|
+
Requires-Dist: termcolor>=1.1.0; extra == "all"
|
|
29
|
+
Requires-Dist: fsspec>=2023.3.0; extra == "all"
|
|
30
|
+
Provides-Extra: io
|
|
31
|
+
Requires-Dist: fsspec>=2023.3.0; extra == "io"
|
|
26
32
|
Dynamic: author
|
|
27
33
|
Dynamic: author-email
|
|
28
34
|
Dynamic: classifier
|
|
@@ -32,6 +38,7 @@ Dynamic: home-page
|
|
|
32
38
|
Dynamic: keywords
|
|
33
39
|
Dynamic: license
|
|
34
40
|
Dynamic: license-file
|
|
41
|
+
Dynamic: provides-extra
|
|
35
42
|
Dynamic: requires-dist
|
|
36
43
|
Dynamic: summary
|
|
37
44
|
|
|
@@ -56,8 +56,8 @@ pyglove/core/hyper/numerical_test.py,sha256=UWdH55Bok7bghYDLJOGsgOwV_2LNkhj1AmFw
|
|
|
56
56
|
pyglove/core/hyper/object_template.py,sha256=YPALTV0mMULa7iuqnryTpA2wMsdyFZ_6g-R525asAr8,22222
|
|
57
57
|
pyglove/core/hyper/object_template_test.py,sha256=TEFX7LIqUvdCdJILnK_gP5xIgNJKzRnioUF0CGVBzcY,9105
|
|
58
58
|
pyglove/core/io/__init__.py,sha256=4ZT1a595DqQuLTNYc2JP_eCp_KesXvHmKRkr777bzpg,785
|
|
59
|
-
pyglove/core/io/file_system.py,sha256=
|
|
60
|
-
pyglove/core/io/file_system_test.py,sha256=
|
|
59
|
+
pyglove/core/io/file_system.py,sha256=r7x_kDWDynKbAcO8tEkkRHShOPiYXyB9wmCXv2c-Oes,23149
|
|
60
|
+
pyglove/core/io/file_system_test.py,sha256=QvcPEQj3hAk7xc8VkFda1ZzgsV0etBimc1V9CJXr_Rk,19145
|
|
61
61
|
pyglove/core/io/sequence.py,sha256=XdVpPBuvhUnrTIWMUrak_qdcNdUJBpjgH5c1b5I3E2A,8873
|
|
62
62
|
pyglove/core/io/sequence_test.py,sha256=mnONyNG1M1sCae2tyI-tF8qns96htfZPKycthETPthU,4062
|
|
63
63
|
pyglove/core/patching/__init__.py,sha256=C1Q1cWPV74YL3eXbzGvc-8aPw1DR8EK6lRhQYDCwHek,2059
|
|
@@ -67,8 +67,8 @@ pyglove/core/patching/pattern_based.py,sha256=UtSNB-ARNqVjXwZovjVi84QEoXUGLLBTgL
|
|
|
67
67
|
pyglove/core/patching/pattern_based_test.py,sha256=PW1EcVfsFPB6wtgwg3s4dzvigWn3b5S8eMNGo0SJiZ0,2771
|
|
68
68
|
pyglove/core/patching/rule_based.py,sha256=JAQp8mWeIOxwIdqusA3GmXia-fxQhQsxbUTmE329wF8,17038
|
|
69
69
|
pyglove/core/patching/rule_based_test.py,sha256=qfy0ILmczV_LMHWEnwo2y079OrJsGYO0nKxSZdmIUcI,18782
|
|
70
|
-
pyglove/core/symbolic/__init__.py,sha256=
|
|
71
|
-
pyglove/core/symbolic/base.py,sha256=
|
|
70
|
+
pyglove/core/symbolic/__init__.py,sha256=ifUnJyF_hNFyrNAqlN7ClKWUvZouIA38GrYctnMc-F4,6402
|
|
71
|
+
pyglove/core/symbolic/base.py,sha256=QMAMFuqH_FjwJUSlUZKTBnxDpiVkM9Z8j611u6FWAcg,79816
|
|
72
72
|
pyglove/core/symbolic/base_test.py,sha256=OHEexXI7uE2bixT-trfR3j-dfiAJsbY7pFyAA6XEPqA,7338
|
|
73
73
|
pyglove/core/symbolic/boilerplate.py,sha256=sQ3G25r5bo_UmIdjreL4jkAuQCXIHVlvUfGjjkNod6Y,5955
|
|
74
74
|
pyglove/core/symbolic/boilerplate_test.py,sha256=1CZ1W6kq3l-3tpaknhGFa04V18bO7vPzis5qzWnxHEs,5252
|
|
@@ -91,9 +91,9 @@ pyglove/core/symbolic/functor_test.py,sha256=9c5_7OBKNVNbYC7IaVQB6c5ks2v00qQ36oi
|
|
|
91
91
|
pyglove/core/symbolic/inferred.py,sha256=E4zgphg6NNZad9Fl3jdHQOMZeqEp9XHq5OUYqXEmwZQ,3178
|
|
92
92
|
pyglove/core/symbolic/inferred_test.py,sha256=G6uPykONcChvs6vZujXHSWaYfjewLTVBscMqzzKNty0,1270
|
|
93
93
|
pyglove/core/symbolic/list.py,sha256=CRDoBxYJsmNly1MxhY5vO0wp6EUTnxze6-2O9vXHna4,30717
|
|
94
|
-
pyglove/core/symbolic/list_test.py,sha256=
|
|
95
|
-
pyglove/core/symbolic/object.py,sha256=
|
|
96
|
-
pyglove/core/symbolic/object_test.py,sha256=
|
|
94
|
+
pyglove/core/symbolic/list_test.py,sha256=sH3wuJB6SqXVefJEPmzGVfoXc-ZiJ1ZTV4-O88LFuaM,61943
|
|
95
|
+
pyglove/core/symbolic/object.py,sha256=g6omG6r6B5TQaHSgoOWCEL_2OY_DcutxOTe1R1M1uFo,42822
|
|
96
|
+
pyglove/core/symbolic/object_test.py,sha256=mB8Aw1foE3XWdRX1LVn9C9U5XJeR7iqZYHrv4y94STE,94514
|
|
97
97
|
pyglove/core/symbolic/origin.py,sha256=OSWMKjvPcISOXrzuX3lCQC8m_qaGl-9INsIB81erUnU,6124
|
|
98
98
|
pyglove/core/symbolic/origin_test.py,sha256=dU_ZGrGDetM_lYVMn3wQO0d367_t_t8eESe3NrKPBNE,3159
|
|
99
99
|
pyglove/core/symbolic/pure_symbolic.py,sha256=pvo15gn35_KLiGW_XrTjlx5ddmHbwpLr93VgbQ59uQ8,3231
|
|
@@ -101,6 +101,8 @@ pyglove/core/symbolic/ref.py,sha256=gIu02b8BfKspH1XejXhEFh_Iil3jvfGHdpaCRq6qor0,
|
|
|
101
101
|
pyglove/core/symbolic/ref_test.py,sha256=-rCA1AaLZnyuKOh0cJzS5UaQ_9Kp4p7xexZ_e3IwpOg,8974
|
|
102
102
|
pyglove/core/symbolic/symbolize.py,sha256=ohID9-V8QiFe7OMpPlRomiqUnKBVMpypd8ZuMuHaa4s,6582
|
|
103
103
|
pyglove/core/symbolic/symbolize_test.py,sha256=o7bRfMhGc6uw2FIH8arE99-bPb3i0YixcHYyiP-QqeQ,6487
|
|
104
|
+
pyglove/core/symbolic/unknown_symbols.py,sha256=-BuT1izzGZDjuz_OSPDz9UAd1ialpxK8ceTZtIdni1Y,4323
|
|
105
|
+
pyglove/core/symbolic/unknown_symbols_test.py,sha256=0bjZ4cYjDdTCHwQ5GHe-D7VVDfJez7iG0v6Z0ksEDTU,3069
|
|
104
106
|
pyglove/core/tuning/__init__.py,sha256=JtXpjsBto01fLf55hZ1dSx-CEZUyVQeyRP9AMH_hw8c,2229
|
|
105
107
|
pyglove/core/tuning/backend.py,sha256=GwbBI9nHYYe5h6iDuhqRtD-Msjmmc7wFtagxW0rTypU,5597
|
|
106
108
|
pyglove/core/tuning/backend_test.py,sha256=vyfUi509-hbF5uYk-I0FsoniPFBXmobQVl-4d0YPIWY,1999
|
|
@@ -113,8 +115,8 @@ pyglove/core/tuning/sample_test.py,sha256=JqwDPy3EPC_VjU9dipk90jj1kovZB3Zb9hAjAl
|
|
|
113
115
|
pyglove/core/typing/__init__.py,sha256=u2YSrSi8diTkQn8_1J2hEpk5o7zDhx2tU_oRuS-k1XU,14580
|
|
114
116
|
pyglove/core/typing/annotated.py,sha256=llaajIDj9GK-4kUGJoO4JsHU6ESPOra2SZ-jG6xmsOQ,3203
|
|
115
117
|
pyglove/core/typing/annotated_test.py,sha256=p1qid3R-jeiOTTxOVq6hXW8XFvn-h1cUzJWISPst2l8,2484
|
|
116
|
-
pyglove/core/typing/annotation_conversion.py,sha256=
|
|
117
|
-
pyglove/core/typing/annotation_conversion_test.py,sha256=
|
|
118
|
+
pyglove/core/typing/annotation_conversion.py,sha256=pnm6dZbn_nsTCreyarspkwpsdteMb8uFT2TLtblq_0M,15780
|
|
119
|
+
pyglove/core/typing/annotation_conversion_test.py,sha256=N49ZuzsmaqVi23wszDpfwcJApr2-Pk2E_ELrjssSQb0,17598
|
|
118
120
|
pyglove/core/typing/annotation_future_test.py,sha256=tAVuzWNfW8R4e4l7fx88Q4nJDM2LPUogNKNAIIPAEWQ,3959
|
|
119
121
|
pyglove/core/typing/callable_ext.py,sha256=PiBQWPeUAH7Lgmf2xKCZqgK7N0OSrTdbnEkV8Ph31OA,9127
|
|
120
122
|
pyglove/core/typing/callable_ext_test.py,sha256=TnWKU4_ZjvpbHZFtFHgFvCMDiCos8VmLlODcM_7Xg8M,10156
|
|
@@ -130,12 +132,12 @@ pyglove/core/typing/json_schema_test.py,sha256=ZxMO2xgKiELNDzoQ84cmXsyCtFA0Ltn1I
|
|
|
130
132
|
pyglove/core/typing/key_specs.py,sha256=-7xjCuUGoQgD0sMafsRFNlw3S4f1r-7t5OO4ev5bbeI,9225
|
|
131
133
|
pyglove/core/typing/key_specs_test.py,sha256=5zornpyHMAYoRaG8KDXHiQ3obu9UfRp3399lBeUNTPk,6499
|
|
132
134
|
pyglove/core/typing/pytype_support.py,sha256=lyX11WVbCwoOi5tTQ90pEOS-yvo_6iEi7Lxbp-nXu2A,2069
|
|
133
|
-
pyglove/core/typing/type_conversion.py,sha256=
|
|
134
|
-
pyglove/core/typing/type_conversion_test.py,sha256=
|
|
135
|
+
pyglove/core/typing/type_conversion.py,sha256=S_57FU-9DOk-MTE-1Mh31FMshaA3IoKiSDsoKxGxGv4,5432
|
|
136
|
+
pyglove/core/typing/type_conversion_test.py,sha256=rK0lAjLi1azKY4ZltquIsCpKh20EtYSIekArnqI6ThQ,5475
|
|
135
137
|
pyglove/core/typing/typed_missing.py,sha256=-l1omAu0jBZv5BnsFYXBqfvQwVBnmPh_X1wcIKD9bOk,2734
|
|
136
138
|
pyglove/core/typing/typed_missing_test.py,sha256=TCNsb1SRpFaVdxYn2mB_yaLuja8w5Qn5NP7uGiZVBWs,2301
|
|
137
|
-
pyglove/core/typing/value_specs.py,sha256=
|
|
138
|
-
pyglove/core/typing/value_specs_test.py,sha256=
|
|
139
|
+
pyglove/core/typing/value_specs.py,sha256=oaLjvJ61Gv7myn93krzG3kfdw0n9va3sxmyDjy0xRFY,103368
|
|
140
|
+
pyglove/core/typing/value_specs_test.py,sha256=Mi0Esw-0E_1JSjoEQcjkefkcaS0-omIBzlWVTRYv1F8,131170
|
|
139
141
|
pyglove/core/utils/__init__.py,sha256=6P2VcGkjDsOFG640Jqu-jd1K3pfAK5NkcK3NBPLI6RY,8726
|
|
140
142
|
pyglove/core/utils/common_traits.py,sha256=PWxOgPhG5H60ZwfO8xNAEGRjFUqqDZQBWQYomOfvdy8,3640
|
|
141
143
|
pyglove/core/utils/common_traits_test.py,sha256=DIuZB_1xfmeTVfWnGOguDQcDAM_iGgBOe8C-5CsIqBc,1122
|
|
@@ -149,8 +151,8 @@ pyglove/core/utils/formatting.py,sha256=Wn4d933LQLhuMIfjdRJgpxOThCxBxQrkRBa6Z1-h
|
|
|
149
151
|
pyglove/core/utils/formatting_test.py,sha256=hhg-nL6DyE5A2QA92ALHK5QtfAYKfPpTbBARF-IT1j0,14241
|
|
150
152
|
pyglove/core/utils/hierarchical.py,sha256=jwB-0FhqOspAymAkvJphRhPTQEsoShmKupCZpU3Vip4,19690
|
|
151
153
|
pyglove/core/utils/hierarchical_test.py,sha256=f382DMJPa_bavJGGQDjuw-hWcafUg5bkQCPX-nbzeiI,21077
|
|
152
|
-
pyglove/core/utils/json_conversion.py,sha256=
|
|
153
|
-
pyglove/core/utils/json_conversion_test.py,sha256=
|
|
154
|
+
pyglove/core/utils/json_conversion.py,sha256=PWV51Dm7J-ovakhqXJux_zBAFqvjHKb9RObER8phDvw,37165
|
|
155
|
+
pyglove/core/utils/json_conversion_test.py,sha256=w4k6qgXGEnNXrfryTQppxtF3GJ9rdEHjvjznXSIOMcg,15809
|
|
154
156
|
pyglove/core/utils/missing.py,sha256=9gslt1lXd1qSEIuAFxUWu30oD-YdYcnm13eau1S9uqY,1445
|
|
155
157
|
pyglove/core/utils/missing_test.py,sha256=D6-FuVEwCyJemUiPLcwLmwyptqI5Bx0Pfipc2juhKSE,1335
|
|
156
158
|
pyglove/core/utils/text_color.py,sha256=xcCTCxY2qFNZs_jismMGus8scEXKBpYGAhpAgnz-MHk,4112
|
|
@@ -175,8 +177,8 @@ pyglove/core/views/html/controls/label.py,sha256=2u7z_6o-ANf6EbxufFl_fZ1VFSUrjNw
|
|
|
175
177
|
pyglove/core/views/html/controls/label_test.py,sha256=_Fi6vMITup8iFYTiU_1w7FZCXaYp1eMmVBxub8JMYbs,5170
|
|
176
178
|
pyglove/core/views/html/controls/progress_bar.py,sha256=0an0eCbPCDjwrR58C16NwLZ-cf3Oy0wQerLsiNgGHmk,5235
|
|
177
179
|
pyglove/core/views/html/controls/progress_bar_test.py,sha256=kKOJDZQtBPkmNcgIBrRQkNNzcTm51ojuFBTRUEDSsp0,3506
|
|
178
|
-
pyglove/core/views/html/controls/tab.py,sha256=
|
|
179
|
-
pyglove/core/views/html/controls/tab_test.py,sha256=
|
|
180
|
+
pyglove/core/views/html/controls/tab.py,sha256=f3ZfYyXfMmERGOXY4a4bxmOhomL3MQONMossaxw8hQw,11851
|
|
181
|
+
pyglove/core/views/html/controls/tab_test.py,sha256=V0HrY0YaGyVlOykMCFg85QSxOq8eBPa3Fqcv2q1c12s,6912
|
|
180
182
|
pyglove/core/views/html/controls/tooltip.py,sha256=01BbpuM1twf3FYMUT09_Ck5JSSONe8QE9RmyA9nhCnU,3092
|
|
181
183
|
pyglove/core/views/html/controls/tooltip_test.py,sha256=17BY-WmZKpz9tCbySPcwG6KJyfeE_MeMyKxtfxorBQ0,3194
|
|
182
184
|
pyglove/ext/__init__.py,sha256=3jp8cJvKW6PENOZlmVAbT0w-GBRn_kjhc0wDX3XjpOE,755
|
|
@@ -187,7 +189,7 @@ pyglove/ext/early_stopping/step_wise.py,sha256=P99Z2hODmCNBnR3iVOOj2NCCwveSH6h5V
|
|
|
187
189
|
pyglove/ext/early_stopping/step_wise_test.py,sha256=I9DDMrCpDwIWC6mV9w2pDypnrwYnWjg6QXTFNT13cts,9032
|
|
188
190
|
pyglove/ext/evolution/__init__.py,sha256=lAf4NyxUZRt39kMFFoW_i8-ExigJXakG1-sUREW7jkQ,3214
|
|
189
191
|
pyglove/ext/evolution/base.py,sha256=I27qJja6MErMs3SyrlBYvmDQ4eTq9dY9RVpscKlwReQ,50090
|
|
190
|
-
pyglove/ext/evolution/base_test.py,sha256=
|
|
192
|
+
pyglove/ext/evolution/base_test.py,sha256=HgWSGGHh7nd03IFLln3dKV4TsI1kK4idJd4azAGaxHw,29943
|
|
191
193
|
pyglove/ext/evolution/hill_climb.py,sha256=Bysi2u4KEM7d9CIPcnvKKgEQHttkaKFkI8xlrNOkBB8,1688
|
|
192
194
|
pyglove/ext/evolution/hill_climb_test.py,sha256=7snzopGFRgkryNXiDVcHMhtVz6LXLZCOH_tz_t15b4I,3110
|
|
193
195
|
pyglove/ext/evolution/mutators.py,sha256=ZkNmIf9B2KRP3H7UfML7nkjUYPPgOkm5Fe1kaJQ8W5I,10062
|
|
@@ -218,8 +220,8 @@ pyglove/ext/scalars/randoms.py,sha256=LkMIIx7lOq_lvJvVS3BrgWGuWl7Pi91-lA-O8x_gZs
|
|
|
218
220
|
pyglove/ext/scalars/randoms_test.py,sha256=nEhiqarg8l_5EOucp59CYrpO2uKxS1pe0hmBdZUzRNM,2000
|
|
219
221
|
pyglove/ext/scalars/step_wise.py,sha256=IDw3tuTpv0KVh7AN44W43zqm1-E0HWPUlytWOQC9w3Y,3789
|
|
220
222
|
pyglove/ext/scalars/step_wise_test.py,sha256=TL1vJ19xVx2t5HKuyIzGoogF7N3Rm8YhLE6JF7i0iy8,2540
|
|
221
|
-
pyglove-0.5.0.
|
|
222
|
-
pyglove-0.5.0.
|
|
223
|
-
pyglove-0.5.0.
|
|
224
|
-
pyglove-0.5.0.
|
|
225
|
-
pyglove-0.5.0.
|
|
223
|
+
pyglove-0.5.0.dev202511300809.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
224
|
+
pyglove-0.5.0.dev202511300809.dist-info/METADATA,sha256=UenEruuR6yfx1_hUKjd-L5lbN15GMZnpOhKYeA7hwPc,7349
|
|
225
|
+
pyglove-0.5.0.dev202511300809.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
226
|
+
pyglove-0.5.0.dev202511300809.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
|
|
227
|
+
pyglove-0.5.0.dev202511300809.dist-info/RECORD,,
|
|
File without changes
|
{pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{pyglove-0.5.0.dev202510230131.dist-info → pyglove-0.5.0.dev202511300809.dist-info}/top_level.txt
RENAMED
|
File without changes
|