pyglove 0.4.5.dev202410170809__py3-none-any.whl → 0.4.5.dev202410190807__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 +1 -0
- pyglove/core/symbolic/base.py +10 -10
- pyglove/core/symbolic/base_test.py +47 -8
- pyglove/core/symbolic/diff.py +60 -66
- pyglove/core/symbolic/diff_test.py +33 -98
- pyglove/core/symbolic/ref.py +17 -3
- pyglove/core/symbolic/ref_test.py +9 -110
- pyglove/core/views/__init__.py +1 -0
- pyglove/core/views/base.py +45 -151
- pyglove/core/views/base_test.py +14 -49
- pyglove/core/views/html/base.py +19 -18
- pyglove/core/views/html/base_test.py +10 -8
- pyglove/core/views/html/tree_view.py +911 -561
- pyglove/core/views/html/tree_view_test.py +743 -164
- {pyglove-0.4.5.dev202410170809.dist-info → pyglove-0.4.5.dev202410190807.dist-info}/METADATA +1 -1
- {pyglove-0.4.5.dev202410170809.dist-info → pyglove-0.4.5.dev202410190807.dist-info}/RECORD +19 -19
- {pyglove-0.4.5.dev202410170809.dist-info → pyglove-0.4.5.dev202410190807.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202410170809.dist-info → pyglove-0.4.5.dev202410190807.dist-info}/WHEEL +0 -0
- {pyglove-0.4.5.dev202410170809.dist-info → pyglove-0.4.5.dev202410190807.dist-info}/top_level.txt +0 -0
pyglove/core/views/base.py
CHANGED
@@ -133,7 +133,7 @@ import inspect
|
|
133
133
|
import io
|
134
134
|
import os
|
135
135
|
import types
|
136
|
-
from typing import Any, Callable,
|
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
|
-
|
440
|
-
|
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
|
-
**
|
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
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
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, **
|
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__'
|
pyglove/core/views/base_test.py
CHANGED
@@ -287,8 +287,8 @@ class TestView(View):
|
|
287
287
|
self,
|
288
288
|
*,
|
289
289
|
view: 'TestView',
|
290
|
-
use_summary: bool =
|
291
|
-
use_content: bool =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
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
|
499
|
-
self.assertEqual(
|
500
|
-
|
501
|
-
|
502
|
-
|
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')
|
pyglove/core/views/html/base.py
CHANGED
@@ -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
|
-
|
289
|
-
|
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
|
-
|
300
|
-
|
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
|
-
|
312
|
+
css_classes = cls.concate(css_classes)
|
313
313
|
options = cls.concate(options)
|
314
|
-
|
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="{
|
319
|
-
f' style="{
|
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
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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',
|
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',
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
792
|
+
css_classes='my_class',
|
791
793
|
).add_style('div.my_class { color: red; }')
|
792
794
|
]
|
793
795
|
)),
|