pyglove 0.4.5.dev202410170809__py3-none-any.whl → 0.4.5.dev202410180346__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.
@@ -133,7 +133,7 @@ import inspect
133
133
  import io
134
134
  import os
135
135
  import types
136
- from typing import Any, Callable, ContextManager, Dict, Iterator, Optional, Sequence, Set, Type, Union
136
+ from typing import Any, Callable, Dict, Iterator, Optional, Sequence, Set, Type, Union
137
137
 
138
138
  from pyglove.core import io as pg_io
139
139
  from pyglove.core import object_utils
@@ -151,6 +151,12 @@ NodeFilter = Callable[
151
151
  ]
152
152
 
153
153
 
154
+ _VIEW_ARGS_PRESET_NAME = 'pyglove_view_args'
155
+ _VIEW_REGISTRY = {}
156
+ _TLS_KEY_OPERAND_STACK_BY_METHOD = '__view_operand_stack__'
157
+ _TLS_KEY_VIEW_OPTIONS = '__view_options__'
158
+
159
+
154
160
  class Content(object_utils.Formattable, metaclass=abc.ABCMeta):
155
161
  """Content: A type of media to be displayed in a view.
156
162
 
@@ -436,14 +442,39 @@ def view(
436
442
  if isinstance(value, Content):
437
443
  return value
438
444
 
439
- view_object = View.create(view_id)
440
- with View.preset_args(**kwargs):
445
+ with view_options(**kwargs) as options:
446
+ view_object = View.create(view_id)
441
447
  return view_object.render(
442
448
  value, name=name, root_path=root_path or object_utils.KeyPath(),
443
- **kwargs
449
+ **options
444
450
  )
445
451
 
446
452
 
453
+ @contextlib.contextmanager
454
+ def view_options(**kwargs) -> Iterator[Dict[str, Any]]:
455
+ """Context manager to inject rendering args to view.
456
+
457
+ Example:
458
+
459
+ with pg.view_options(enable_summary_tooltip=False):
460
+ MyObject().to_html()
461
+
462
+ Args:
463
+ **kwargs: Keyword arguments for View.render method.
464
+
465
+ Yields:
466
+ The merged keyword arguments.
467
+ """
468
+ parent_options = object_utils.thread_local_peek(_TLS_KEY_VIEW_OPTIONS, {})
469
+ # Deep merge the two dict.
470
+ options = object_utils.merge([parent_options, kwargs])
471
+ object_utils.thread_local_push(_TLS_KEY_VIEW_OPTIONS, options)
472
+ try:
473
+ yield options
474
+ finally:
475
+ object_utils.thread_local_pop(_TLS_KEY_VIEW_OPTIONS)
476
+
477
+
447
478
  class View(metaclass=abc.ABCMeta):
448
479
  """Base class for views.
449
480
 
@@ -457,60 +488,6 @@ class View(metaclass=abc.ABCMeta):
457
488
  # argument of `pg.view()`. Must be set for non-abstract subclasses.
458
489
  VIEW_ID = None
459
490
 
460
- class PresetArgValue(pg_typing.PresetArgValue):
461
- """Argument marker for View and Extension methods to use preset values.
462
-
463
- Methods defined in a ``View`` or ``Extension`` class can be parameterized by
464
- keyword arguments from the ``pg.view()`` function. These arguments, referred
465
- to as preset arguments, allow the View or Extension method to consume
466
- parameters passed during object rendering. For instance, when rendering a
467
- user-defined object ``MyObject()`` with a keyword argument
468
- ``hide_default=True``, the View or Extension method may use this argument to
469
- customize the rendering behavior.
470
-
471
- Since there can be multiple layers of calls between ``View`` and
472
- ``Extension`` methods, it is easy to lose track of arguments passed from
473
- ``pg.view()``. To simplify this process, users can employ the
474
- ``PresetArgValue`` marker to annotate specific arguments. This ensures that
475
- View or Extension methods can access preset arguments from the parent call
476
- to ``pg.view()``, even when those arguments are not explicitly passed around
477
- in subsequent method calls.
478
-
479
- Example usage:
480
-
481
- .. code-block:: python
482
-
483
- class MyView(pg.View):
484
- VIEW_TYPE = 'my_view'
485
-
486
- class Extension(pg.View.Extension):
487
- def _myview_render(
488
- self,
489
- *,
490
- view,
491
- hide_default_value=pg.View.PresetArgValue(default=False),
492
- **kwargs
493
- ):
494
- return view.render(value, **kwargs)
495
-
496
- @pg.View.extension_method('_myview_render')
497
- def render(self, value, *, **kwargs):
498
- return ...
499
-
500
- In the above example, the ``hide_default_value`` argument is annotated with
501
- ``pg.View.PresetArgValue``. This allows it to consume the
502
- ``hide_default=True`` keyword argument from the parent call to
503
- ``pg.view()``, or fallback to the default value ``False`` if the argument is
504
- not provided.
505
- """
506
-
507
- @classmethod
508
- def preset_args(cls, **kwargs) -> ContextManager[Dict[str, Any]]:
509
- """Context manager to provide preset argument values for a specific view."""
510
- return pg_typing.preset_args(
511
- kwargs, preset_name=_VIEW_ARGS_PRESET_NAME, inherit_preset=True
512
- )
513
-
514
491
  class Extension:
515
492
  """Extension for the View class.
516
493
 
@@ -563,31 +540,6 @@ class View(metaclass=abc.ABCMeta):
563
540
  methods to provide custom view rendering for the object.
564
541
  """
565
542
 
566
- def __init_subclass__(cls):
567
- # Enable preset args for all methods. As a result, function args
568
- # with `View.PresetArgValue` will be able to consume the keyword args
569
- # from the call to `pg.view`.
570
- supported_preset_args = {}
571
- for name, method in cls.__dict__.items():
572
- if inspect.isfunction(method):
573
- used_preset_args = View.PresetArgValue.inspect(method)
574
- if used_preset_args:
575
- supported_preset_args.update(used_preset_args)
576
- setattr(
577
- cls, name,
578
- pg_typing.enable_preset_args(
579
- include_all_preset_kwargs=True,
580
- preset_name=_VIEW_ARGS_PRESET_NAME,
581
- )(method)
582
- )
583
- setattr(cls, '_SUPPORTED_PRESET_ARGS', supported_preset_args)
584
- super().__init_subclass__()
585
-
586
- @classmethod
587
- def supported_preset_args(cls) -> Dict[str, Any]:
588
- """Returns supported preset args for this view."""
589
- return getattr(cls, '_SUPPORTED_PRESET_ARGS', {})
590
-
591
543
  @classmethod
592
544
  @functools.cache
593
545
  def supported_view_classes(cls) -> Set[Type['View']]:
@@ -674,6 +626,7 @@ class View(metaclass=abc.ABCMeta):
674
626
  sig.args[i].name: arg
675
627
  for i, arg in enumerate(args) if i != extension_arg_index
676
628
  })
629
+ kwargs.pop('value', None)
677
630
  return kwargs
678
631
 
679
632
  # We use the original function signature to generate the view method.
@@ -682,14 +635,13 @@ class View(metaclass=abc.ABCMeta):
682
635
  # This allows a View method to consume the preset kwargs from the
683
636
  # parent call to `pg.view`, yet also customizes the preset kwargs
684
637
  # to the calls to other View/Extension methods within this context.
685
- with cls.preset_args(**kwargs):
686
- return self._maybe_dispatch( # pylint: disable=protected-access
687
- *args, **kwargs,
688
- extension=get_extension(args, kwargs),
689
- view_method=func,
690
- extension_method_name=method_name,
691
- arg_map_fn=map_args
692
- )
638
+ return self._maybe_dispatch( # pylint: disable=protected-access
639
+ *args, **kwargs,
640
+ extension=get_extension(args, kwargs),
641
+ view_method=func,
642
+ extension_method_name=method_name,
643
+ arg_map_fn=map_args
644
+ )
693
645
  return _generated_view_fn
694
646
  return decorator
695
647
 
@@ -709,62 +661,8 @@ class View(metaclass=abc.ABCMeta):
709
661
  )
710
662
 
711
663
  _VIEW_REGISTRY[cls.VIEW_ID] = cls
712
-
713
- # Enable preset args for all view methods. As a result, function args
714
- # with `pg.views.View.PresetArgValue` will be able to consume the keyword
715
- # args from the parent call to `pg.view`.
716
- supported_preset_args = {}
717
- for name, method in cls.__dict__.items():
718
- if inspect.isfunction(method):
719
- used_preset_args = cls.PresetArgValue.inspect(method)
720
- if used_preset_args:
721
- setattr(
722
- cls, name,
723
- pg_typing.enable_preset_args(
724
- include_all_preset_kwargs=True,
725
- preset_name=_VIEW_ARGS_PRESET_NAME,
726
- )(method)
727
- )
728
- supported_preset_args.update(used_preset_args)
729
-
730
- setattr(cls, '_SUPPORTED_PRESET_ARGS', supported_preset_args)
731
664
  super().__init_subclass__()
732
665
 
733
- @classmethod
734
- def supported_preset_args(
735
- cls,
736
- view_id: Optional[str] = None,
737
- include_all_extensions: bool = True
738
- ) -> Dict[Type[Any], Dict[str, Any]]:
739
- """Returns all supported preset args for this view.
740
-
741
- Args:
742
- view_id: The view ID to get the supported preset args for. If not
743
- provided, the default view ID for this class will be used.
744
- include_all_extensions: Whether to include the supported preset args from
745
- all extensions of this view. If False, only the supported preset args
746
- from the base extension class will be included.
747
-
748
- Returns:
749
- A dictionary mapping view classes to their supported preset args.
750
- """
751
- view_id = view_id or cls.VIEW_ID
752
- if view_id is None:
753
- raise ValueError(
754
- 'No `VIEW_ID` is set for this View class. Please set `VIEW_ID` '
755
- 'or provide a `view_id` to `supported_preset_args`.'
756
- )
757
- supported = {cls: getattr(cls, '_SUPPORTED_PRESET_ARGS', {})}
758
- extension_classes = [cls.Extension]
759
- if include_all_extensions:
760
- extension_classes.extend(cls.Extension.__subclasses__())
761
-
762
- for cls in extension_classes:
763
- supported_preset_args = cls.supported_preset_args()
764
- if supported_preset_args:
765
- supported[cls] = supported_preset_args
766
- return supported
767
-
768
666
  @classmethod
769
667
  def dir(cls) -> Dict[str, Type['View']]:
770
668
  """Returns all registered View classes with their view IDs."""
@@ -872,8 +770,9 @@ class View(metaclass=abc.ABCMeta):
872
770
  return view_method(self, *args, **kwargs)
873
771
 
874
772
  # Call the extension's method.
773
+ mapped = arg_map_fn(args, kwargs)
875
774
  return getattr(extension, extension_method_name)(
876
- view=self, **arg_map_fn(args, kwargs)
775
+ view=self, **mapped
877
776
  )
878
777
 
879
778
  @contextlib.contextmanager
@@ -901,8 +800,3 @@ class View(metaclass=abc.ABCMeta):
901
800
  object_utils.thread_local_del(_TLS_KEY_OPERAND_STACK_BY_METHOD)
902
801
  else:
903
802
  rendering_stack[view_method] = callsite_value
904
-
905
-
906
- _VIEW_ARGS_PRESET_NAME = 'view_args'
907
- _VIEW_REGISTRY = {}
908
- _TLS_KEY_OPERAND_STACK_BY_METHOD = '__view_operand_stack__'
@@ -287,8 +287,8 @@ class TestView(View):
287
287
  self,
288
288
  *,
289
289
  view: 'TestView',
290
- use_summary: bool = View.PresetArgValue(False),
291
- use_content: bool = View.PresetArgValue(False),
290
+ use_summary: bool = False,
291
+ use_content: bool = False,
292
292
  **kwargs):
293
293
  if use_summary:
294
294
  return view.summary(self, **kwargs)
@@ -299,7 +299,7 @@ class TestView(View):
299
299
  def _test_view_summary(
300
300
  self,
301
301
  view: 'TestView',
302
- default_behavior: bool = View.PresetArgValue(False),
302
+ default_behavior: bool = False,
303
303
  **kwargs):
304
304
  if default_behavior:
305
305
  return view.summary(self, **kwargs)
@@ -309,7 +309,7 @@ class TestView(View):
309
309
  del kwargs
310
310
  return Document(f'[Custom Content] {self}')
311
311
 
312
- def bar(self, x: int = View.PresetArgValue(2)):
312
+ def bar(self, x: int = 2):
313
313
  return x
314
314
 
315
315
  @View.extension_method('_test_view_render')
@@ -317,7 +317,7 @@ class TestView(View):
317
317
  self,
318
318
  value: Any,
319
319
  *,
320
- greeting: str = View.PresetArgValue('hi'),
320
+ greeting: str = 'hi',
321
321
  **kwargs):
322
322
  return Document(f'{greeting} {value}')
323
323
 
@@ -325,7 +325,7 @@ class TestView(View):
325
325
  def summary(
326
326
  self,
327
327
  value: Any,
328
- title: str = View.PresetArgValue('abc'),
328
+ title: str = 'abc',
329
329
  **kwargs
330
330
  ):
331
331
  del kwargs
@@ -340,7 +340,7 @@ class TestView(View):
340
340
  del kwargs
341
341
  return Document(f'[Default Key] {key}: {value}')
342
342
 
343
- def foo(self, x: int = View.PresetArgValue(1)):
343
+ def foo(self, x: int = 1):
344
344
  return x
345
345
 
346
346
 
@@ -378,7 +378,7 @@ class MyObject(TestView.Extension, TestView2.Extension):
378
378
  self.y = y
379
379
 
380
380
  def _test_view_content(
381
- self, *, text: str = View.PresetArgValue('MyObject'), **kwargs
381
+ self, *, text: str = 'MyObject', **kwargs
382
382
  ):
383
383
  return Document(f'[{text}] {self.x} {self.y}')
384
384
 
@@ -459,49 +459,14 @@ class ViewTest(unittest.TestCase):
459
459
  {TestView, TestView2}
460
460
  )
461
461
 
462
- def test_supported_preset_args(self):
463
- self.assertEqual(
464
- TestView.supported_preset_args(),
465
- {
466
- TestView: {
467
- 'greeting': View.PresetArgValue('hi'),
468
- 'title': View.PresetArgValue('abc'),
469
- 'x': View.PresetArgValue(1),
470
- },
471
- TestView.Extension: {
472
- 'default_behavior': View.PresetArgValue(default=False),
473
- 'use_content': View.PresetArgValue(False),
474
- 'use_summary': View.PresetArgValue(False),
475
- 'x': View.PresetArgValue(2),
476
- },
477
- MyObject: {
478
- 'text': View.PresetArgValue('MyObject'),
479
- },
480
- }
481
- )
482
- self.assertEqual(
483
- TestView2.supported_preset_args(),
484
- {
485
- TestView2: {},
486
- MyObject: {
487
- 'text': View.PresetArgValue('MyObject'),
488
- },
489
- }
490
- )
491
-
492
- with self.assertRaisesRegex(ValueError, 'No `VIEW_ID` is set'):
493
- View.supported_preset_args()
494
-
495
- def test_view_method_using_preset_args(self):
462
+ def test_view_options(self):
496
463
  view = View.create('test')
497
464
  self.assertEqual(view.foo(), 1)
498
- with View.preset_args(x=2):
499
- self.assertEqual(view.foo(), 2)
500
-
501
- def test_node_method_using_preset_args(self):
502
- self.assertEqual(MyObject(1, '').bar(), 2)
503
- with View.preset_args(x=3):
504
- self.assertEqual(MyObject(1, '').bar(), 3)
465
+ with base.view_options(text='hello', use_content=True):
466
+ self.assertEqual(
467
+ base.view(MyObject(1, 'a'), view_id='test').content,
468
+ '[hello] 1 a'
469
+ )
505
470
 
506
471
  def test_view_default_behavior(self):
507
472
  view = View.create('test')
@@ -285,8 +285,8 @@ class Html(base.Content):
285
285
  inner_html: Optional[List[WritableTypes]] = None,
286
286
  *,
287
287
  options: Union[str, Iterable[str], None] = None,
288
- css_class: NestableStr = None,
289
- style: Union[str, Dict[str, Any], None] = None,
288
+ css_classes: NestableStr = None,
289
+ styles: Union[str, Dict[str, Any], None] = None,
290
290
  **properties
291
291
  ) -> 'Html':
292
292
  """Creates an HTML element.
@@ -296,8 +296,8 @@ class Html(base.Content):
296
296
  inner_html: The inner HTML of the element.
297
297
  options: Positional options that will be added to the element. E.g. 'open'
298
298
  for `<details open>`.
299
- css_class: The CSS class name or a list of CSS class names.
300
- style: A single CSS style string or a dictionary of CSS properties.
299
+ css_classes: The CSS class name or a list of CSS class names.
300
+ styles: A single CSS style string or a dictionary of CSS properties.
301
301
  **properties: Keyword arguments for HTML properties. For properties with
302
302
  underscore in the name, the underscore will be replaced by dash in the
303
303
  generated HTML. E.g. `background_color` will be converted to
@@ -309,14 +309,14 @@ class Html(base.Content):
309
309
  s = cls()
310
310
 
311
311
  # Write the open tag.
312
- css_class = cls.concate(css_class)
312
+ css_classes = cls.concate(css_classes)
313
313
  options = cls.concate(options)
314
- style = cls.style_str(style)
314
+ styles = cls.style_str(styles)
315
315
  s.write(
316
316
  f'<{tag}',
317
317
  f' {options}' if options else None,
318
- f' class="{css_class}"' if css_class else None,
319
- f' style="{style}"' if style else None,
318
+ f' class="{css_classes}"' if css_classes else None,
319
+ f' style="{styles}"' if styles else None,
320
320
  )
321
321
  for k, v in properties.items():
322
322
  if v is not None:
@@ -351,18 +351,19 @@ class Html(base.Content):
351
351
 
352
352
  @classmethod
353
353
  def concate(
354
- cls, nestable_str: NestableStr, separator: str = ' '
354
+ cls, nestable_str: NestableStr, separator: str = ' ', dedup: bool = True
355
355
  ) -> Optional[str]:
356
356
  """Concates the string nodes in a nestable object."""
357
- if isinstance(nestable_str, str):
358
- return nestable_str
359
- elif isinstance(nestable_str, list):
360
- flattened = [cls.concate(s) for s in nestable_str]
361
- flattened = [s for s in flattened if s is not None]
362
- return separator.join(flattened) if flattened else None
363
- else:
364
- assert nestable_str is None, nestable_str
365
- return None
357
+ flattened = object_utils.flatten(nestable_str)
358
+ if isinstance(flattened, str):
359
+ return flattened
360
+ elif isinstance(flattened, dict):
361
+ str_items = [v for v in flattened.values() if isinstance(v, str)]
362
+ if dedup:
363
+ str_items = list(dict.fromkeys(str_items).keys())
364
+ if str_items:
365
+ return separator.join(str_items)
366
+ return None
366
367
 
367
368
  @classmethod
368
369
  def style_str(
@@ -701,6 +701,8 @@ class HtmlTest(TestCase):
701
701
  self.assertIsNone(Html.concate([None, [None, [None, None]]]))
702
702
  self.assertEqual(Html.concate('a'), 'a')
703
703
  self.assertEqual(Html.concate(['a']), 'a')
704
+ self.assertEqual(Html.concate(['a', 'a']), 'a')
705
+ self.assertEqual(Html.concate(['a', 'a'], dedup=False), 'a a')
704
706
  self.assertEqual(Html.concate(['a', None, 'b']), 'a b')
705
707
  self.assertEqual(
706
708
  Html.concate(['a', 'b', [None, 'c', [None, 'd']]]), 'a b c d')
@@ -710,11 +712,11 @@ class HtmlTest(TestCase):
710
712
  self.assertEqual(Html.element('div').content, '<div></div>')
711
713
  # CSS class as list.
712
714
  self.assertEqual(
713
- Html.element('div', css_class=['a', 'b', None]).content,
715
+ Html.element('div', css_classes=['a', 'b', None]).content,
714
716
  '<div class="a b"></div>',
715
717
  )
716
718
  self.assertEqual(
717
- Html.element('div', css_class=[None, None]).content,
719
+ Html.element('div', css_classes=[None, None]).content,
718
720
  '<div></div>',
719
721
  )
720
722
  # Style as string.
@@ -726,7 +728,7 @@ class HtmlTest(TestCase):
726
728
  self.assertEqual(
727
729
  Html.element(
728
730
  'div',
729
- style=dict(
731
+ styles=dict(
730
732
  color='red', background_color='blue', width=None,
731
733
  )
732
734
  ).content,
@@ -735,7 +737,7 @@ class HtmlTest(TestCase):
735
737
  self.assertEqual(
736
738
  Html.element(
737
739
  'div',
738
- style=dict(
740
+ styles=dict(
739
741
  color=None,
740
742
  )
741
743
  ).content,
@@ -746,7 +748,7 @@ class HtmlTest(TestCase):
746
748
  Html.element(
747
749
  'details',
748
750
  options='open',
749
- css_class='my_class',
751
+ css_classes='my_class',
750
752
  id='my_id',
751
753
  custom_property='1'
752
754
  ).content,
@@ -759,7 +761,7 @@ class HtmlTest(TestCase):
759
761
  Html.element(
760
762
  'details',
761
763
  options=[None],
762
- css_class='my_class',
764
+ css_classes='my_class',
763
765
  id='my_id',
764
766
  custom_property='1'
765
767
  ).content,
@@ -772,7 +774,7 @@ class HtmlTest(TestCase):
772
774
  self.assertEqual(
773
775
  Html.element(
774
776
  'div',
775
- css_class='my_class',
777
+ css_classes='my_class',
776
778
  inner_html='<h1>foo</h1>'
777
779
  ).content,
778
780
  '<div class="my_class"><h1>foo</h1></div>',
@@ -787,7 +789,7 @@ class HtmlTest(TestCase):
787
789
  None,
788
790
  Html.element(
789
791
  'div',
790
- css_class='my_class',
792
+ css_classes='my_class',
791
793
  ).add_style('div.my_class { color: red; }')
792
794
  ]
793
795
  )),