pyglove 0.4.5.dev202504280824__py3-none-any.whl → 0.4.5.dev202504300810__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.
@@ -2458,7 +2458,8 @@ def symbolic_transform_fn(allow_partial: bool):
2458
2458
  return value
2459
2459
  if isinstance(value, dict):
2460
2460
  value_spec = pg_typing.ensure_value_spec(
2461
- field.value, pg_typing.Dict(), path)
2461
+ field.value, pg_typing.Dict().noneable(), path
2462
+ )
2462
2463
  value = Symbolic.DictType( # pytype: disable=not-callable # pylint: disable=not-callable
2463
2464
  value,
2464
2465
  value_spec=value_spec,
@@ -2471,7 +2472,10 @@ def symbolic_transform_fn(allow_partial: bool):
2471
2472
  pass_through=True)
2472
2473
  elif isinstance(value, list):
2473
2474
  value_spec = pg_typing.ensure_value_spec(
2474
- field.value, pg_typing.List(pg_typing.Any()), path)
2475
+ field.value,
2476
+ pg_typing.List(pg_typing.Any()).noneable(),
2477
+ path
2478
+ )
2475
2479
  value = Symbolic.ListType( # pytype: disable=not-callable # pylint: disable=not-callable
2476
2480
  value,
2477
2481
  value_spec=value_spec,
@@ -407,7 +407,7 @@ def _value_spec_from_type_annotation(
407
407
  else:
408
408
  spec = vs.Union([_sub_value_spec_from_annotation(x) for x in args])
409
409
  if optional:
410
- spec = spec.noneable()
410
+ spec = spec.noneable(use_none_as_default=False)
411
411
  return spec
412
412
  elif origin is typing.Final:
413
413
  return _value_spec_from_type_annotation(
@@ -459,11 +459,11 @@ class ValueSpecFromAnnotationTest(unittest.TestCase):
459
459
  def test_optional(self):
460
460
  self.assertEqual(
461
461
  ValueSpec.from_annotation(typing.Optional[int], True),
462
- vs.Int().noneable())
462
+ vs.Int().noneable(use_none_as_default=False))
463
463
  if annotation_conversion._UnionType:
464
464
  self.assertEqual(
465
465
  ValueSpec.from_annotation(int | None, True),
466
- vs.Int().noneable())
466
+ vs.Int().noneable(use_none_as_default=False))
467
467
 
468
468
  def test_union(self):
469
469
  self.assertEqual(
@@ -471,11 +471,11 @@ class ValueSpecFromAnnotationTest(unittest.TestCase):
471
471
  vs.Union([vs.Int(), vs.Str()]))
472
472
  self.assertEqual(
473
473
  ValueSpec.from_annotation(typing.Union[int, str, None], True),
474
- vs.Union([vs.Int(), vs.Str()]).noneable())
474
+ vs.Union([vs.Int(), vs.Str()]).noneable(use_none_as_default=False))
475
475
  if annotation_conversion._UnionType:
476
476
  self.assertEqual(
477
- ValueSpec.from_annotation(int | str, True),
478
- vs.Union([vs.Int(), vs.Str()]))
477
+ ValueSpec.from_annotation(int | str | None, True),
478
+ vs.Union([vs.Int(), vs.Str()]).noneable(use_none_as_default=False))
479
479
 
480
480
  def test_final(self):
481
481
  self.assertEqual(
@@ -67,7 +67,9 @@ class AnnotationFutureConversionTest(unittest.TestCase):
67
67
  class Bar(pg.Object):
68
68
  x: list[int | None]
69
69
 
70
- self.assert_value_spec(Bar, 'x', vs.List(vs.Int().noneable()))
70
+ self.assert_value_spec(
71
+ Bar, 'x', vs.List(vs.Int().noneable(use_none_as_default=False))
72
+ )
71
73
 
72
74
  def test_var_length_tuple(self):
73
75
 
@@ -88,13 +90,17 @@ class AnnotationFutureConversionTest(unittest.TestCase):
88
90
  class Foo(pg.Object):
89
91
  x: typing.Optional[int]
90
92
 
91
- self.assert_value_spec(Foo, 'x', vs.Int().noneable())
93
+ self.assert_value_spec(
94
+ Foo, 'x', vs.Int().noneable(use_none_as_default=False)
95
+ )
92
96
 
93
97
  if sys.version_info >= (3, 10):
94
98
  class Bar(pg.Object):
95
99
  x: int | None
96
100
 
97
- self.assert_value_spec(Bar, 'x', vs.Int().noneable())
101
+ self.assert_value_spec(
102
+ Bar, 'x', vs.Int().noneable(use_none_as_default=False)
103
+ )
98
104
 
99
105
  def test_union(self):
100
106
 
@@ -102,7 +108,11 @@ class AnnotationFutureConversionTest(unittest.TestCase):
102
108
  x: Union[int, typing.Union[str, bool], None]
103
109
 
104
110
  self.assert_value_spec(
105
- Foo, 'x', vs.Union([vs.Int(), vs.Str(), vs.Bool()]).noneable()
111
+ Foo,
112
+ 'x',
113
+ vs.Union(
114
+ [vs.Int(), vs.Str(), vs.Bool()]
115
+ ).noneable(use_none_as_default=False)
106
116
  )
107
117
 
108
118
  if sys.version_info >= (3, 10):
@@ -125,7 +135,7 @@ class AnnotationFutureConversionTest(unittest.TestCase):
125
135
 
126
136
  def test_self_referencial(self):
127
137
  self.assert_value_spec(
128
- self.A, 'a', vs.Object(self.A).noneable()
138
+ self.A, 'a', vs.Object(self.A).noneable(use_none_as_default=False)
129
139
  )
130
140
  self.assert_value_spec(
131
141
  self.A, 'b', vs.List(vs.Object(self.A))
@@ -370,7 +370,10 @@ class ValueSpec(utils.Formattable, utils.JSONConvertible):
370
370
  """Returns forward referenes used by the value spec."""
371
371
 
372
372
  @abc.abstractmethod
373
- def noneable(self) -> 'ValueSpec':
373
+ def noneable(
374
+ self,
375
+ is_noneable=True,
376
+ use_none_as_default: bool = True) -> 'ValueSpec':
374
377
  """Marks none-able and returns `self`."""
375
378
 
376
379
  @property
@@ -885,20 +885,31 @@ class CreateSchemaTest(unittest.TestCase):
885
885
  self.assertEqual(s['e'], Field('e', vs.Enum(0, [0, 1])))
886
886
  self.assertEqual(s.metadata, {'user_data': 2})
887
887
  self.assertEqual(s['f'], Field('f', vs.Int()))
888
- self.assertEqual(s['f1'], Field('f1', vs.Int().noneable()))
888
+ self.assertEqual(
889
+ s['f1'], Field('f1', vs.Int().noneable(use_none_as_default=False))
890
+ )
889
891
  self.assertEqual(s['g'], Field('g', vs.Float()))
890
- self.assertEqual(s['g1'], Field('g1', vs.Float().noneable()))
892
+ self.assertEqual(
893
+ s['g1'], Field('g1', vs.Float().noneable(use_none_as_default=False))
894
+ )
891
895
  self.assertEqual(s['h'], Field('h', vs.Bool()))
892
- self.assertEqual(s['h1'], Field('h1', vs.Bool().noneable()))
896
+ self.assertEqual(
897
+ s['h1'], Field('h1', vs.Bool().noneable(use_none_as_default=False))
898
+ )
893
899
  self.assertEqual(s['i'], Field('i', vs.Str()))
894
- self.assertEqual(s['i1'], Field('i1', vs.Str().noneable()))
900
+ self.assertEqual(
901
+ s['i1'], Field('i1', vs.Str().noneable(use_none_as_default=False))
902
+ )
895
903
  self.assertEqual(
896
904
  s['j'], Field('j', vs.Union([vs.Int(), vs.Float(), vs.Bool()]))
897
905
  )
898
906
  self.assertEqual(s['k'], Field('k', vs.List(vs.Any())))
899
907
  self.assertEqual(s['l'], Field('l', vs.List(vs.Int())))
900
908
  self.assertEqual(s['L'], Field('L', vs.List(vs.Int())))
901
- self.assertEqual(s['l1'], Field('l1', vs.List(vs.Int()).noneable()))
909
+ self.assertEqual(
910
+ s['l1'],
911
+ Field('l1', vs.List(vs.Int()).noneable(use_none_as_default=False))
912
+ )
902
913
  self.assertEqual(
903
914
  s['l2'],
904
915
  Field('l2', vs.List(vs.Str()).set_default(['black', 'white'])),
@@ -140,11 +140,18 @@ class ValueSpecBase(ValueSpec):
140
140
  """Returns True if current value spec accepts None."""
141
141
  return self._is_noneable
142
142
 
143
- def noneable(self) -> 'ValueSpecBase':
143
+ def noneable(
144
+ self,
145
+ is_noneable: bool = True,
146
+ use_none_as_default: bool = True
147
+ ) -> 'ValueSpecBase':
144
148
  """Marks None is acceptable and returns `self`."""
145
- self._is_noneable = True
146
- if MISSING_VALUE == self._default: # pytype: disable=attribute-error
147
- self._default = None
149
+ self._is_noneable = is_noneable
150
+ if is_noneable:
151
+ if use_none_as_default and not self.has_default: # pytype: disable=attribute-error
152
+ self.set_default(None, False)
153
+ elif self.default is None:
154
+ self.set_default(MISSING_VALUE, False)
148
155
  return self
149
156
 
150
157
  @property
@@ -942,11 +949,21 @@ class Enum(Generic, PrimitiveType):
942
949
  return self.default
943
950
  return self.apply(*args)
944
951
 
945
- def noneable(self) -> 'Enum':
952
+ def noneable(
953
+ self,
954
+ is_noneable: bool = True,
955
+ use_none_as_default: bool = True
956
+ ) -> 'Enum':
946
957
  """Noneable is specially treated for Enum."""
947
- if None not in self._values:
948
- self._values.append(None)
949
- self._is_noneable = True
958
+ if is_noneable:
959
+ if None not in self._values:
960
+ self._values.append(None)
961
+ else:
962
+ if None in self._values:
963
+ self._values.remove(None)
964
+ if self._default is None:
965
+ self._default = MISSING_VALUE
966
+ self._is_noneable = is_noneable
950
967
  return self
951
968
 
952
969
  @property
@@ -1714,10 +1731,19 @@ class Dict(Generic, ValueSpecBase):
1714
1731
  """Returns the schema of this dict spec."""
1715
1732
  return self._schema
1716
1733
 
1717
- def noneable(self) -> 'Dict':
1734
+ def noneable(
1735
+ self,
1736
+ is_noneable: bool = True,
1737
+ use_none_as_default: bool = True
1738
+ ) -> 'Dict':
1718
1739
  """Override noneable in Dict to always set default value None."""
1719
- self._is_noneable = True
1720
- self.set_default(None, False)
1740
+ self._is_noneable = is_noneable
1741
+ if is_noneable:
1742
+ if use_none_as_default:
1743
+ self.set_default(None, False)
1744
+ elif self._default is None:
1745
+ # Automatically generate default based on schema.
1746
+ self.set_default(MISSING_VALUE)
1721
1747
  return self
1722
1748
 
1723
1749
  def set_default(
@@ -2698,11 +2724,18 @@ class Union(Generic, ValueSpecBase):
2698
2724
  value_types.update(child_value_type)
2699
2725
  return tuple(value_types)
2700
2726
 
2701
- def noneable(self) -> 'Union':
2727
+ def noneable(
2728
+ self,
2729
+ is_noneable: bool = True,
2730
+ use_none_as_default: bool = True
2731
+ ) -> 'Union':
2702
2732
  """Customized noneable for Union."""
2703
- super().noneable()
2733
+ super().noneable(
2734
+ is_noneable=is_noneable,
2735
+ use_none_as_default=use_none_as_default
2736
+ )
2704
2737
  for c in self._candidates:
2705
- c.noneable()
2738
+ c.noneable(is_noneable=is_noneable, use_none_as_default=False)
2706
2739
  return self
2707
2740
 
2708
2741
  @property
@@ -61,6 +61,10 @@ class BoolTest(ValueSpecTest):
61
61
  def test_noneable(self):
62
62
  self.assertFalse(vs.Bool().is_noneable)
63
63
  self.assertTrue(vs.Bool().noneable().is_noneable)
64
+ self.assertIsNone(vs.Bool().noneable().default)
65
+ self.assertFalse(vs.Bool().noneable(use_none_as_default=False).has_default)
66
+ self.assertFalse(vs.Bool().noneable().noneable(False).is_noneable)
67
+ self.assertFalse(vs.Bool().noneable().noneable(False).has_default)
64
68
 
65
69
  def test_repr(self):
66
70
  self.assertEqual(repr(vs.Bool()), 'Bool()')
@@ -212,6 +216,10 @@ class StrTest(ValueSpecTest):
212
216
  def test_noneable(self):
213
217
  self.assertFalse(vs.Str().is_noneable)
214
218
  self.assertTrue(vs.Str().noneable().is_noneable)
219
+ self.assertIsNone(vs.Str().noneable().default)
220
+ self.assertFalse(vs.Str().noneable(use_none_as_default=False).has_default)
221
+ self.assertFalse(vs.Str().noneable().noneable(False).is_noneable)
222
+ self.assertFalse(vs.Str().noneable().noneable(False).has_default)
215
223
 
216
224
  def test_repr(self):
217
225
  self.assertEqual(repr(vs.Str()), 'Str()')
@@ -371,6 +379,10 @@ class IntTest(ValueSpecTest):
371
379
  def test_noneable(self):
372
380
  self.assertFalse(vs.Int().is_noneable)
373
381
  self.assertTrue(vs.Int().noneable().is_noneable)
382
+ self.assertIsNone(vs.Int().noneable().default)
383
+ self.assertFalse(vs.Int().noneable(use_none_as_default=False).has_default)
384
+ self.assertFalse(vs.Int().noneable().noneable(False).is_noneable)
385
+ self.assertFalse(vs.Int().noneable().noneable(False).has_default)
374
386
 
375
387
  def test_repr(self):
376
388
  self.assertEqual(repr(vs.Int()), 'Int()')
@@ -584,6 +596,10 @@ class FloatTest(ValueSpecTest):
584
596
  def test_noneable(self):
585
597
  self.assertFalse(vs.Float().is_noneable)
586
598
  self.assertTrue(vs.Float().noneable().is_noneable)
599
+ self.assertIsNone(vs.Float().noneable().default)
600
+ self.assertFalse(vs.Float().noneable(use_none_as_default=False).has_default)
601
+ self.assertFalse(vs.Float().noneable().noneable(False).is_noneable)
602
+ self.assertFalse(vs.Float().noneable().noneable(False).has_default)
587
603
 
588
604
  def test_repr(self):
589
605
  self.assertEqual(str(vs.Float()), 'Float()')
@@ -809,6 +825,18 @@ class EnumTest(ValueSpecTest):
809
825
  self.assertEqual(
810
826
  vs.Enum('a', ['a', 'b']).noneable(),
811
827
  vs.Enum('a', ['a', 'b', None]))
828
+ self.assertEqual(
829
+ vs.Enum('a', ['a', 'b']).noneable().default,
830
+ 'a'
831
+ )
832
+ self.assertEqual(
833
+ vs.Enum('a', ['a', None]).noneable(False),
834
+ vs.Enum('a', ['a'])
835
+ )
836
+ self.assertEqual(
837
+ vs.Enum(None, [None, 'a']).noneable(False),
838
+ vs.Enum(typed_missing.MISSING_VALUE, ['a'])
839
+ )
812
840
 
813
841
  def test_repr(self):
814
842
  self.assertEqual(
@@ -960,6 +988,12 @@ class ListTest(ValueSpecTest):
960
988
  def test_noneable(self):
961
989
  self.assertFalse(vs.List(vs.Int()).is_noneable)
962
990
  self.assertTrue(vs.List(vs.Int()).noneable().is_noneable)
991
+ self.assertIsNone(vs.List(vs.Int()).noneable().default)
992
+ self.assertFalse(
993
+ vs.List(vs.Int()).noneable(use_none_as_default=False).has_default
994
+ )
995
+ self.assertFalse(vs.List(vs.Int()).noneable().noneable(False).is_noneable)
996
+ self.assertFalse(vs.List(vs.Int()).noneable().noneable(False).has_default)
963
997
 
964
998
  def test_str(self):
965
999
  self.assertEqual(
@@ -1268,6 +1302,12 @@ class TupleTest(ValueSpecTest):
1268
1302
  def test_noneable(self):
1269
1303
  self.assertFalse(vs.Tuple([vs.Int()]).is_noneable)
1270
1304
  self.assertTrue(vs.Tuple([vs.Int()]).noneable().is_noneable)
1305
+ self.assertFalse(
1306
+ vs.Tuple([vs.Int()]).noneable().noneable(False).is_noneable
1307
+ )
1308
+ self.assertFalse(
1309
+ vs.Tuple([vs.Int()]).noneable().noneable(False).has_default
1310
+ )
1271
1311
 
1272
1312
  def test_fixed_length(self):
1273
1313
  self.assertFalse(vs.Tuple(vs.Int()).fixed_length)
@@ -1750,6 +1790,12 @@ class DictTest(ValueSpecTest):
1750
1790
  def test_noneable(self):
1751
1791
  self.assertFalse(vs.Dict().is_noneable)
1752
1792
  self.assertTrue(vs.Dict().noneable().is_noneable)
1793
+ self.assertFalse(vs.Dict().noneable().noneable(False).is_noneable)
1794
+ self.assertFalse(vs.Dict().noneable().noneable(False).has_default)
1795
+ self.assertEqual(
1796
+ vs.Dict({'x': vs.Int(default=1)}).noneable().noneable(False).default,
1797
+ {'x': 1}
1798
+ )
1753
1799
 
1754
1800
  def test_repr(self):
1755
1801
  self.assertEqual(repr(vs.Dict()), 'Dict()')
@@ -2156,6 +2202,8 @@ class ObjectTest(ValueSpecTest):
2156
2202
  self.assertFalse(vs.Object(self.A).is_noneable)
2157
2203
  self.assertTrue(vs.Object(self.A).noneable().is_noneable)
2158
2204
  self.assertTrue(vs.Object('Foo').noneable().is_noneable)
2205
+ self.assertFalse(vs.Object(self.A).noneable(False).is_noneable)
2206
+ self.assertFalse(vs.Object(self.A).noneable(False).has_default)
2159
2207
 
2160
2208
  def test_repr(self):
2161
2209
  self.assertEqual(repr(vs.Object(self.A)), 'Object(A)')
@@ -2416,6 +2464,8 @@ class CallableTest(ValueSpecTest):
2416
2464
  def test_noneable(self):
2417
2465
  self.assertFalse(vs.Callable().is_noneable)
2418
2466
  self.assertTrue(vs.Callable().noneable().is_noneable)
2467
+ self.assertFalse(vs.Callable().noneable().noneable(False).is_noneable)
2468
+ self.assertFalse(vs.Callable().noneable().noneable(False).has_default)
2419
2469
 
2420
2470
  def test_repr(self):
2421
2471
  self.assertEqual(repr(vs.Callable()), 'Callable()')
@@ -2861,6 +2911,8 @@ class TypeTest(ValueSpecTest):
2861
2911
  def test_noneable(self):
2862
2912
  self.assertFalse(vs.Type(Exception).is_noneable)
2863
2913
  self.assertTrue(vs.Type(Exception).noneable().is_noneable)
2914
+ self.assertFalse(vs.Type(Exception).noneable(False).is_noneable)
2915
+ self.assertFalse(vs.Type(Exception).noneable(False).has_default)
2864
2916
 
2865
2917
  def test_repr(self):
2866
2918
  self.assertEqual(repr(vs.Type(Exception)), 'Type(<class \'Exception\'>)')
@@ -3123,6 +3175,41 @@ class UnionTest(ValueSpecTest):
3123
3175
  )
3124
3176
  self.assertTrue(
3125
3177
  vs.Union([vs.Int().noneable(), vs.Bool()]).is_noneable)
3178
+ self.assertFalse(
3179
+ vs.Union([vs.Int().noneable(), vs.Bool()]).has_default)
3180
+ self.assertTrue(
3181
+ vs.Union([vs.Int().noneable(), vs.Bool()]).candidates[0].has_default
3182
+ )
3183
+ self.assertIsNone(
3184
+ vs.Union(
3185
+ [vs.Int(), vs.Bool()]
3186
+ ).noneable().default
3187
+ )
3188
+ self.assertFalse(
3189
+ vs.Union(
3190
+ [vs.Int(), vs.Bool()]
3191
+ ).noneable(use_none_as_default=False).has_default
3192
+ )
3193
+ self.assertTrue(
3194
+ vs.Union(
3195
+ [vs.Int(), vs.Bool()]
3196
+ ).noneable().candidates[0].is_noneable
3197
+ )
3198
+ self.assertFalse(
3199
+ vs.Union(
3200
+ [vs.Int(), vs.Bool()]
3201
+ ).noneable().candidates[0].has_default
3202
+ )
3203
+ self.assertFalse(
3204
+ vs.Union(
3205
+ [vs.Int().noneable(), vs.Bool()]
3206
+ ).noneable(False).candidates[0].is_noneable
3207
+ )
3208
+ self.assertFalse(
3209
+ vs.Union(
3210
+ [vs.Int().noneable(), vs.Bool()]
3211
+ ).noneable(False).candidates[0].has_default
3212
+ )
3126
3213
 
3127
3214
  def test_str(self):
3128
3215
  self.assertEqual(
@@ -3132,8 +3219,8 @@ class UnionTest(ValueSpecTest):
3132
3219
  'Union([Int(), Bool()], default=1, frozen=True)')
3133
3220
  self.assertEqual(
3134
3221
  repr(vs.Union([vs.Int(), vs.Bool()], default=1).noneable()),
3135
- 'Union([Int(default=None, noneable=True), '
3136
- 'Bool(default=None, noneable=True)], default=1, noneable=True)')
3222
+ 'Union([Int(noneable=True), '
3223
+ 'Bool(noneable=True)], default=1, noneable=True)')
3137
3224
 
3138
3225
  def test_annotation(self):
3139
3226
  self.assertEqual(
@@ -3436,6 +3523,11 @@ class AnyTest(ValueSpecTest):
3436
3523
 
3437
3524
  def test_noneable(self):
3438
3525
  self.assertTrue(vs.Any().is_noneable)
3526
+ self.assertFalse(vs.Any().has_default)
3527
+ self.assertTrue(vs.Any().noneable().is_noneable)
3528
+ self.assertTrue(vs.Any().noneable().has_default)
3529
+ self.assertFalse(vs.Any().noneable(False).is_noneable)
3530
+ self.assertFalse(vs.Any().noneable().noneable(False).has_default)
3439
3531
 
3440
3532
  def test_repr(self):
3441
3533
  self.assertEqual(repr(vs.Any()), 'Any()')
@@ -18,7 +18,7 @@ import functools
18
18
  import html as html_lib
19
19
  import inspect
20
20
  import typing
21
- from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Union
21
+ from typing import Any, Callable, Dict, Iterable, Optional, Sequence, Union
22
22
 
23
23
  from pyglove.core import typing as pg_typing
24
24
  from pyglove.core import utils
@@ -297,7 +297,7 @@ class Html(base.Content):
297
297
  def element(
298
298
  cls,
299
299
  tag: str,
300
- inner_html: Optional[List[WritableTypes]] = None,
300
+ inner_html: Optional[utils.Nestable[WritableTypes]] = None,
301
301
  *,
302
302
  options: Union[str, Iterable[str], None] = None,
303
303
  css_classes: NestableStr = None,
@@ -340,8 +340,11 @@ class Html(base.Content):
340
340
 
341
341
  # Write the inner HTML.
342
342
  if inner_html:
343
- for child in inner_html:
344
- s.write(child)
343
+ if isinstance(inner_html, list):
344
+ for child in utils.flatten(inner_html).values():
345
+ s.write(child)
346
+ else:
347
+ s.write(inner_html)
345
348
 
346
349
  # Write the closing tag.
347
350
  s.write(f'</{tag}>')
@@ -800,12 +800,14 @@ class HtmlTest(TestCase):
800
800
  'div',
801
801
  [
802
802
  '<hr>',
803
- lambda: '<div>bar</div>',
803
+ [[lambda: '<div>bar</div>']],
804
804
  None,
805
- Html.element(
806
- 'div',
807
- css_classes='my_class',
808
- ).add_style('div.my_class { color: red; }')
805
+ [
806
+ Html.element(
807
+ 'div',
808
+ css_classes='my_class',
809
+ ).add_style('div.my_class { color: red; }')
810
+ ]
809
811
  ]
810
812
  )),
811
813
  """
@@ -16,6 +16,7 @@
16
16
  from typing import Annotated, Any, Dict, List, Optional, Union
17
17
 
18
18
  from pyglove.core import typing as pg_typing
19
+ from pyglove.core import utils as pg_utils
19
20
  from pyglove.core.symbolic import flags as pg_flags
20
21
  from pyglove.core.symbolic import object as pg_object
21
22
  # pylint: disable=g-importing-member
@@ -157,6 +158,23 @@ class LabelGroup(HtmlControl):
157
158
  'The label for the name of the group.'
158
159
  ] = None
159
160
 
161
+ @pg_utils.explicit_method_override
162
+ def __init__(
163
+ self,
164
+ labels: List[Union[Label, str, Html, None, List[Any]]],
165
+ name: Optional[Label] = None,
166
+ id: Optional[str] = None, # pylint: disable=redefined-builtin
167
+ css_classes: Optional[List[str]] = None,
168
+ styles: Optional[Dict[str, str]] = None,
169
+ **kwargs,
170
+ ) -> None:
171
+ if labels:
172
+ labels = [l for l in pg_utils.flatten(labels).values() if l is not None]
173
+ super().__init__(
174
+ labels=labels, name=name, id=id, css_classes=css_classes or [],
175
+ styles=styles or {}, **kwargs
176
+ )
177
+
160
178
  def _on_bound(self):
161
179
  super()._on_bound()
162
180
  with pg_flags.notify_on_change(False):
@@ -136,7 +136,7 @@ class LabelTest(TestCase):
136
136
  class LabelGroupTest(TestCase):
137
137
 
138
138
  def test_basic(self):
139
- group = label_lib.LabelGroup(['foo', 'bar'], name='baz')
139
+ group = label_lib.LabelGroup(['foo', None, ['bar']], name='baz')
140
140
  self.assert_html_content(
141
141
  group,
142
142
  (
@@ -13,8 +13,9 @@
13
13
  # limitations under the License.
14
14
  """Tab control."""
15
15
 
16
- from typing import Annotated, List, Literal, Optional, Union
16
+ from typing import Annotated, Any, Dict, List, Literal, Optional, Union
17
17
 
18
+ from pyglove.core import utils as pg_utils
18
19
  from pyglove.core.symbolic import flags as pg_flags
19
20
  from pyglove.core.symbolic import object as pg_object
20
21
 
@@ -73,6 +74,48 @@ class TabControl(HtmlControl):
73
74
 
74
75
  interactive = True
75
76
 
77
+ @pg_utils.explicit_method_override
78
+ def __init__(
79
+ self,
80
+ tabs: List[Union[Tab, None, List[Any]]],
81
+ selected: Union[int, str, List[str]] = 0,
82
+ tab_position: Literal['top', 'left'] = 'top',
83
+ id: Optional[str] = None, # pylint: disable=redefined-builtin
84
+ css_classes: Optional[List[str]] = None,
85
+ styles: Optional[Dict[str, str]] = None,
86
+ **kwargs,
87
+ ):
88
+ if tabs:
89
+ tabs = [t for t in pg_utils.flatten(tabs).values() if t is not None]
90
+ selected = self._find_tab_index(tabs, selected)
91
+ if selected == -1:
92
+ selected = 0
93
+ super().__init__(
94
+ tabs, selected=selected, tab_position=tab_position, id=id,
95
+ css_classes=css_classes or [], styles=styles or {},
96
+ **kwargs
97
+ )
98
+
99
+ @classmethod
100
+ def _find_tab_index(
101
+ cls,
102
+ tabs: List[Tab],
103
+ index_or_name: Union[int, str, List[str]]) -> int:
104
+ """Finds the index of a tab identified by an index or name."""
105
+ if isinstance(index_or_name, int):
106
+ return index_or_name
107
+ if isinstance(index_or_name, str):
108
+ priority_choices = [index_or_name]
109
+ else:
110
+ priority_choices = index_or_name
111
+
112
+ name_index = {t.name: i for i, t in enumerate(tabs)}
113
+ for name in priority_choices:
114
+ index = name_index.get(name)
115
+ if index is not None:
116
+ return index
117
+ return -1
118
+
76
119
  def append(self, tab: Tab) -> None:
77
120
  with pg_flags.notify_on_change(False):
78
121
  self.tabs.append(tab)
@@ -28,15 +28,87 @@ class TabControlTest(unittest.TestCase):
28
28
  print(actual)
29
29
  self.assertEqual(actual, expected)
30
30
 
31
+ def test_find_tab_index(self):
32
+ self.assertEqual(
33
+ tab_lib.TabControl._find_tab_index(
34
+ [
35
+ tab_lib.Tab('foo', base.Html('<h1>foo</h1>')),
36
+ tab_lib.Tab('bar', base.Html('<h1>bar</h1>')),
37
+ ],
38
+ 1
39
+ ),
40
+ 1
41
+ )
42
+ self.assertEqual(
43
+ tab_lib.TabControl._find_tab_index(
44
+ [
45
+ tab_lib.Tab('foo', base.Html('<h1>foo</h1>'), name='foo'),
46
+ tab_lib.Tab('bar', base.Html('<h1>bar</h1>'), name='bar'),
47
+ ],
48
+ 'bar'
49
+ ),
50
+ 1
51
+ )
52
+ self.assertEqual(
53
+ tab_lib.TabControl._find_tab_index(
54
+ [
55
+ tab_lib.Tab('foo', base.Html('<h1>foo</h1>'), name='foo'),
56
+ tab_lib.Tab('bar', base.Html('<h1>bar</h1>'), name='bar'),
57
+ ],
58
+ 'baz'
59
+ ),
60
+ -1
61
+ )
62
+ self.assertEqual(
63
+ tab_lib.TabControl._find_tab_index(
64
+ [
65
+ tab_lib.Tab('foo', base.Html('<h1>foo</h1>'), name='foo'),
66
+ tab_lib.Tab('bar', base.Html('<h1>bar</h1>'), name='bar'),
67
+ tab_lib.Tab('baz', base.Html('<h1>baz</h1>'), name='baz'),
68
+ ],
69
+ ['baz', 'bar']
70
+ ),
71
+ 2
72
+ )
73
+
74
+ def test_init(self):
75
+ tab = tab_lib.TabControl(
76
+ [
77
+ [tab_lib.Tab('foo', base.Html('<h1>foo</h1>'), name='foo')],
78
+ None,
79
+ tab_lib.Tab('bar', base.Html('<h1>bar</h1>'), name='bar'),
80
+ ],
81
+ 1
82
+ )
83
+ self.assertEqual(tab.selected, 1)
84
+ self.assertEqual(tab.tabs[0].name, 'foo')
85
+ self.assertEqual(tab.tabs[1].name, 'bar')
86
+
87
+ # Cannot find tab by name.
88
+ tab = tab_lib.TabControl(
89
+ [
90
+ tab_lib.Tab('foo', base.Html('<h1>foo</h1>'), name='foo'),
91
+ tab_lib.Tab('bar', base.Html('<h1>bar</h1>'), name='bar'),
92
+ ],
93
+ 'baz'
94
+ )
95
+ self.assertEqual(tab.selected, 0)
96
+ self.assertEqual(tab.tabs[0].name, 'foo')
97
+ self.assertEqual(tab.tabs[1].name, 'bar')
98
+
31
99
  def test_basic(self):
32
100
  tab = tab_lib.TabControl([
33
- tab_lib.Tab('foo', base.Html('<h1>foo</h1>'), css_classes=['foo']),
34
- tab_lib.Tab('bar', base.Html('<h1>bar</h1>')),
35
- ])
101
+ tab_lib.Tab(
102
+ 'foo', base.Html('<h1>foo</h1>'), css_classes=['foo'], name='foo'
103
+ ),
104
+ None,
105
+ [tab_lib.Tab('bar', base.Html('<h1>bar</h1>'), name='bar')],
106
+ ], selected='bar')
107
+ self.assertEqual(tab.selected, 1)
36
108
  elem_id = tab.element_id()
37
109
  self.assert_html_content(
38
110
  tab,
39
- f"""<table class="tab-control"><tr><td><div class="tab-button-group top" id="{elem_id}-button-group"><button class="tab-button selected foo" onclick="openTab(event, '{elem_id}', '{elem_id}-0')"><span class="label">foo</span></button><button class="tab-button" onclick="openTab(event, '{elem_id}', '{elem_id}-1')"><span class="label">bar</span></button></div></td></tr><tr><td><div class="tab-content-group top" id="{elem_id}-content-group"><div class="tab-content selected foo" id="{elem_id}-0"><h1>foo</h1></div><div class="tab-content" id="{elem_id}-1"><h1>bar</h1></div></div></td></tr></table>"""
111
+ f"""<table class="tab-control"><tr><td><div class="tab-button-group top" id="{elem_id}-button-group"><button class="tab-button foo" onclick="openTab(event, '{elem_id}', '{elem_id}-0')"><span class="label">foo</span></button><button class="tab-button selected" onclick="openTab(event, '{elem_id}', '{elem_id}-1')"><span class="label">bar</span></button></div></td></tr><tr><td><div class="tab-content-group top" id="{elem_id}-content-group"><div class="tab-content foo" id="{elem_id}-0"><h1>foo</h1></div><div class="tab-content selected" id="{elem_id}-1"><h1>bar</h1></div></div></td></tr></table>"""
40
112
  )
41
113
  with tab.track_scripts() as scripts:
42
114
  tab.extend([
@@ -1409,7 +1409,7 @@ class HtmlTreeView(HtmlView):
1409
1409
  if remove:
1410
1410
  return {
1411
1411
  k: v for k, v in passthrough_kwargs.items()
1412
- if k not in remove
1412
+ if k not in remove # pytype: disable=unsupported-operands
1413
1413
  }
1414
1414
  return passthrough_kwargs
1415
1415
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyglove
3
- Version: 0.4.5.dev202504280824
3
+ Version: 0.4.5.dev202504300810
4
4
  Summary: PyGlove: A library for manipulating Python objects.
5
5
  Home-page: https://github.com/google/pyglove
6
6
  Author: PyGlove Authors
@@ -66,7 +66,7 @@ pyglove/core/patching/pattern_based_test.py,sha256=PW1EcVfsFPB6wtgwg3s4dzvigWn3b
66
66
  pyglove/core/patching/rule_based.py,sha256=JAQp8mWeIOxwIdqusA3GmXia-fxQhQsxbUTmE329wF8,17038
67
67
  pyglove/core/patching/rule_based_test.py,sha256=qfy0ILmczV_LMHWEnwo2y079OrJsGYO0nKxSZdmIUcI,18782
68
68
  pyglove/core/symbolic/__init__.py,sha256=JZvyaEcS1QxA8MaAGANBWMmRTQcGk_6F0kjFxjokJqg,6002
69
- pyglove/core/symbolic/base.py,sha256=aSZPSk7v3Niv6D1HfL_JEEXfI-U1cQnTxJxY9wXem54,77627
69
+ pyglove/core/symbolic/base.py,sha256=3vOHKtanrksBv5B_0HAq2R_CnE355aPx1B9lyJNb5us,77683
70
70
  pyglove/core/symbolic/base_test.py,sha256=yASIHIuWiUB1jf4nN-Y4XOjyvr8eQfRpr7s_1LZsu78,7188
71
71
  pyglove/core/symbolic/boilerplate.py,sha256=sQ3G25r5bo_UmIdjreL4jkAuQCXIHVlvUfGjjkNod6Y,5955
72
72
  pyglove/core/symbolic/boilerplate_test.py,sha256=1CZ1W6kq3l-3tpaknhGFa04V18bO7vPzis5qzWnxHEs,5252
@@ -109,15 +109,15 @@ pyglove/core/tuning/sample_test.py,sha256=JqwDPy3EPC_VjU9dipk90jj1kovZB3Zb9hAjAl
109
109
  pyglove/core/typing/__init__.py,sha256=MwraQ-6K8NYI_xk4V3oaVSkrPqiU9InDtAgXPSRBvik,14494
110
110
  pyglove/core/typing/annotated.py,sha256=llaajIDj9GK-4kUGJoO4JsHU6ESPOra2SZ-jG6xmsOQ,3203
111
111
  pyglove/core/typing/annotated_test.py,sha256=p1qid3R-jeiOTTxOVq6hXW8XFvn-h1cUzJWISPst2l8,2484
112
- pyglove/core/typing/annotation_conversion.py,sha256=7aMxv5AhC5oYteBtVTRp1no16dFgfGFCLbmxVasrdzQ,15557
113
- pyglove/core/typing/annotation_conversion_test.py,sha256=tOUM7k_VNiGfBlSy_r6vMKk0lD8gRNT6dqnzBzvhul4,17422
114
- pyglove/core/typing/annotation_future_test.py,sha256=ZVLU3kheO2V-nrp-m5jrGMLgGMs4S6RWnJbyD9KbHl4,3746
112
+ pyglove/core/typing/annotation_conversion.py,sha256=txUrChAhMNeaukV-PSQEA9BCjtonUQDWFHxnpTE0_K8,15582
113
+ pyglove/core/typing/annotation_conversion_test.py,sha256=GW7e3e00jcYJjJGRvCvMtW4QBHyVwFtZVcETdh1yBS8,17540
114
+ pyglove/core/typing/annotation_future_test.py,sha256=tAVuzWNfW8R4e4l7fx88Q4nJDM2LPUogNKNAIIPAEWQ,3959
115
115
  pyglove/core/typing/callable_ext.py,sha256=PiBQWPeUAH7Lgmf2xKCZqgK7N0OSrTdbnEkV8Ph31OA,9127
116
116
  pyglove/core/typing/callable_ext_test.py,sha256=TnWKU4_ZjvpbHZFtFHgFvCMDiCos8VmLlODcM_7Xg8M,10156
117
117
  pyglove/core/typing/callable_signature.py,sha256=DRpt7aShfkn8pb3SCiZzS_27eHbkQ_d2UB8BUhJjs0Q,27176
118
118
  pyglove/core/typing/callable_signature_test.py,sha256=iQmHsKPhJPQlMikDhEyxKyq7yWyXI9juKCLYgKhrH3U,25145
119
- pyglove/core/typing/class_schema.py,sha256=dKmF1dwv-7RmCjnAb412AciYZL3LJCToU5WTJWYLj3Y,54482
120
- pyglove/core/typing/class_schema_test.py,sha256=RurRdCyPuypKJ7izgcq9zW3JNHgODiJdQvDn0BDZDjU,29353
119
+ pyglove/core/typing/class_schema.py,sha256=u_pxvykZlwyd5ycdfHpWYI8ccttmBbzexrajNVbEVG4,54553
120
+ pyglove/core/typing/class_schema_test.py,sha256=sJkE7ndDSIKb0EUcjZiVFOeJYDI7Hdu2GdPJCMgZxrI,29556
121
121
  pyglove/core/typing/custom_typing.py,sha256=qdnIKHWNt5kZAAFdpQXra8bBu6RljMbbJ_YDG2mhAUA,2205
122
122
  pyglove/core/typing/inspect.py,sha256=VLSz1KAunNm2hx0eEMjiwxKLl9FHlKr9nHelLT25iEA,7726
123
123
  pyglove/core/typing/inspect_test.py,sha256=xclevobF0X8c_B5b1q1dkBJZN1TsVA1RUhk5l25DUCM,10248
@@ -128,8 +128,8 @@ pyglove/core/typing/type_conversion.py,sha256=0L4Cbsw_QiM-gpsn-4y-XLEIvwiUB16Clj
128
128
  pyglove/core/typing/type_conversion_test.py,sha256=BhASOGvtKXmYLWKCELU1RVB_Nmt1V-saSkGogvsNL7E,5342
129
129
  pyglove/core/typing/typed_missing.py,sha256=-l1omAu0jBZv5BnsFYXBqfvQwVBnmPh_X1wcIKD9bOk,2734
130
130
  pyglove/core/typing/typed_missing_test.py,sha256=TCNsb1SRpFaVdxYn2mB_yaLuja8w5Qn5NP7uGiZVBWs,2301
131
- pyglove/core/typing/value_specs.py,sha256=Yxdrz4aURMBrtqUW4to-2Vptc6Z6oqQrLyMBiAZt2bc,102302
132
- pyglove/core/typing/value_specs_test.py,sha256=MsScWDRaN_8pfRDO9MCp9HdUHVm_8wHWyKorWVSnhE4,127165
131
+ pyglove/core/typing/value_specs.py,sha256=8E83QDZMb3lMXhgzfVNt9u6Bg3NPkvpjLXetjkps8UU,103263
132
+ pyglove/core/typing/value_specs_test.py,sha256=eGXVxdduIM-oEaapJS9Kh7WSQHRUFegLIJ1GEzQkKHA,131017
133
133
  pyglove/core/utils/__init__.py,sha256=I2bRTzigU7qJVEATGlLUFkYzkiCBBCCEwrQyhsrRNmI,8602
134
134
  pyglove/core/utils/common_traits.py,sha256=PWxOgPhG5H60ZwfO8xNAEGRjFUqqDZQBWQYomOfvdy8,3640
135
135
  pyglove/core/utils/common_traits_test.py,sha256=DIuZB_1xfmeTVfWnGOguDQcDAM_iGgBOe8C-5CsIqBc,1122
@@ -159,18 +159,18 @@ pyglove/core/views/__init__.py,sha256=gll9ZBRYz4p_-LWOdzSR2a6UTWcJ8nR430trrP0yLC
159
159
  pyglove/core/views/base.py,sha256=VVWlkyUPvZoNlcwMpuxt-AujGnuIJPS4Ym-jGBX0g9I,26262
160
160
  pyglove/core/views/base_test.py,sha256=UKbr_1TANOAnP7V5ICGF0UEkunfSaHiJ4nXZXhA0SaU,16642
161
161
  pyglove/core/views/html/__init__.py,sha256=Ff51MK1AKdzLM0kczTqLR3fRlJSJLDaFUdVyCu_7-eY,1085
162
- pyglove/core/views/html/base.py,sha256=LcRU8Lot_y1RW6fziaCQVVE0gWP0aVzIcO8v71JaX0E,15002
163
- pyglove/core/views/html/base_test.py,sha256=5RVPNm4YTamTFu6yXA1p2lJg3JHQBKNNBoL977qBZvE,22782
164
- pyglove/core/views/html/tree_view.py,sha256=MkQXtNKn88j9NXdhluKbIaHswjh1QtQoPv-CUwUfCNk,52062
162
+ pyglove/core/views/html/base.py,sha256=jwckE6qyKOcbpgkDDYlB5lUKY6iaynENp6sV3kjl5Dc,15113
163
+ pyglove/core/views/html/base_test.py,sha256=vPBP8nX5rodjs2llKznQNMJ8ct6nBmwp3Y1eam7T4VQ,22838
164
+ pyglove/core/views/html/tree_view.py,sha256=TKDPSO0iruasAw68uhTw6aMTjtHAQxIGjhkvNhB4rXE,52102
165
165
  pyglove/core/views/html/tree_view_test.py,sha256=whUorrw0eiDaZsEzGB2B3EN3wx0vLFuNEe2RBU03GeU,74707
166
166
  pyglove/core/views/html/controls/__init__.py,sha256=61qs5pnJPCTECCGBtkbNfIV3KcCu7cxfVNBEtIg1lMo,1318
167
167
  pyglove/core/views/html/controls/base.py,sha256=aSYFEo-I6Yc28TMk1ZFw_Za85xMo8EvgfQLKISsLSe8,7675
168
- pyglove/core/views/html/controls/label.py,sha256=dXcYId7ASuNqkzKsWMjJ0iQtecoSsUlUgynNNQZOFAM,5681
169
- pyglove/core/views/html/controls/label_test.py,sha256=uUYqBSZ0XLOuv4qG20gmoZzA3RxYxpfqT63K0azHEfU,5162
168
+ pyglove/core/views/html/controls/label.py,sha256=2u7z_6o-ANf6EbxufFl_fZ1VFSUrjNwDnrM784FFdNs,6312
169
+ pyglove/core/views/html/controls/label_test.py,sha256=_Fi6vMITup8iFYTiU_1w7FZCXaYp1eMmVBxub8JMYbs,5170
170
170
  pyglove/core/views/html/controls/progress_bar.py,sha256=0an0eCbPCDjwrR58C16NwLZ-cf3Oy0wQerLsiNgGHmk,5235
171
171
  pyglove/core/views/html/controls/progress_bar_test.py,sha256=kKOJDZQtBPkmNcgIBrRQkNNzcTm51ojuFBTRUEDSsp0,3506
172
- pyglove/core/views/html/controls/tab.py,sha256=ELyroRb3qKQ8ULM_BMq2j-tqV_mnHOvrYv2kbuIe65o,9430
173
- pyglove/core/views/html/controls/tab_test.py,sha256=wr1msQjZHxp1TFKQ5LxtppY2Dys736JCmmEJ8WBC3Fo,3531
172
+ pyglove/core/views/html/controls/tab.py,sha256=dn2rs6IBqvjtKvk0BC8ypVYqjjHsqJvP_Bh9y94QjMc,10818
173
+ pyglove/core/views/html/controls/tab_test.py,sha256=deRXg4LM4dzVgods5HVTXznrOWdddF6wrcl1RuhmRCA,5656
174
174
  pyglove/core/views/html/controls/tooltip.py,sha256=01BbpuM1twf3FYMUT09_Ck5JSSONe8QE9RmyA9nhCnU,3092
175
175
  pyglove/core/views/html/controls/tooltip_test.py,sha256=17BY-WmZKpz9tCbySPcwG6KJyfeE_MeMyKxtfxorBQ0,3194
176
176
  pyglove/ext/__init__.py,sha256=3jp8cJvKW6PENOZlmVAbT0w-GBRn_kjhc0wDX3XjpOE,755
@@ -212,8 +212,8 @@ pyglove/ext/scalars/randoms.py,sha256=LkMIIx7lOq_lvJvVS3BrgWGuWl7Pi91-lA-O8x_gZs
212
212
  pyglove/ext/scalars/randoms_test.py,sha256=nEhiqarg8l_5EOucp59CYrpO2uKxS1pe0hmBdZUzRNM,2000
213
213
  pyglove/ext/scalars/step_wise.py,sha256=IDw3tuTpv0KVh7AN44W43zqm1-E0HWPUlytWOQC9w3Y,3789
214
214
  pyglove/ext/scalars/step_wise_test.py,sha256=TL1vJ19xVx2t5HKuyIzGoogF7N3Rm8YhLE6JF7i0iy8,2540
215
- pyglove-0.4.5.dev202504280824.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
216
- pyglove-0.4.5.dev202504280824.dist-info/METADATA,sha256=amyqZ0_zjYZSu1RxugmrxvX5jRDz7DZ77jpdMK-3iDs,7089
217
- pyglove-0.4.5.dev202504280824.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
218
- pyglove-0.4.5.dev202504280824.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
219
- pyglove-0.4.5.dev202504280824.dist-info/RECORD,,
215
+ pyglove-0.4.5.dev202504300810.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
216
+ pyglove-0.4.5.dev202504300810.dist-info/METADATA,sha256=vipGZt0dKLshbJ9kyOhGBG0ven_rk7lFpUe-X7seIL0,7089
217
+ pyglove-0.4.5.dev202504300810.dist-info/WHEEL,sha256=ooBFpIzZCPdw3uqIQsOo4qqbA4ZRPxHnOH7peeONza0,91
218
+ pyglove-0.4.5.dev202504300810.dist-info/top_level.txt,sha256=wITzJSKcj8GZUkbq-MvUQnFadkiuAv_qv5qQMw0fIow,8
219
+ pyglove-0.4.5.dev202504300810.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.0.0)
2
+ Generator: setuptools (80.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5