pyglove 0.4.5.dev202411132359__py3-none-any.whl → 0.4.5.dev202501250807__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 +40 -21
- pyglove/core/coding/__init__.py +42 -0
- pyglove/core/coding/errors.py +111 -0
- pyglove/core/coding/errors_test.py +98 -0
- pyglove/core/coding/execution.py +312 -0
- pyglove/core/coding/execution_test.py +333 -0
- pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
- pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
- pyglove/core/coding/parsing.py +153 -0
- pyglove/core/coding/parsing_test.py +150 -0
- pyglove/core/coding/permissions.py +100 -0
- pyglove/core/coding/permissions_test.py +93 -0
- pyglove/core/geno/base.py +53 -38
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +36 -27
- pyglove/core/geno/custom.py +18 -15
- pyglove/core/geno/numerical.py +19 -16
- pyglove/core/geno/space.py +3 -4
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +91 -52
- pyglove/core/hyper/custom.py +7 -7
- pyglove/core/hyper/custom_test.py +9 -10
- pyglove/core/hyper/derived.py +30 -22
- pyglove/core/hyper/derived_test.py +3 -5
- pyglove/core/hyper/dynamic_evaluation.py +3 -4
- pyglove/core/hyper/evolvable.py +57 -46
- pyglove/core/hyper/numerical.py +48 -24
- pyglove/core/hyper/numerical_test.py +9 -9
- pyglove/core/hyper/object_template.py +58 -46
- pyglove/core/logging_test.py +0 -2
- pyglove/core/patching/object_factory.py +4 -4
- pyglove/core/patching/pattern_based.py +4 -4
- pyglove/core/patching/rule_based.py +4 -3
- pyglove/core/symbolic/__init__.py +4 -0
- pyglove/core/symbolic/base.py +200 -136
- pyglove/core/symbolic/base_test.py +17 -19
- pyglove/core/symbolic/boilerplate.py +4 -5
- pyglove/core/symbolic/class_wrapper.py +10 -14
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +2 -2
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/contextual_object.py +288 -0
- pyglove/core/symbolic/contextual_object_test.py +327 -0
- pyglove/core/symbolic/dict.py +115 -87
- pyglove/core/symbolic/dict_test.py +188 -131
- pyglove/core/symbolic/diff.py +12 -12
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +16 -15
- pyglove/core/symbolic/functor_test.py +2 -4
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +70 -47
- pyglove/core/symbolic/list_test.py +117 -98
- pyglove/core/symbolic/object.py +59 -58
- pyglove/core/symbolic/object_test.py +143 -90
- pyglove/core/symbolic/origin.py +5 -7
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +33 -16
- pyglove/core/symbolic/ref_test.py +17 -0
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/typing/annotation_conversion.py +8 -3
- pyglove/core/typing/annotation_conversion_test.py +8 -0
- pyglove/core/typing/callable_ext.py +11 -13
- pyglove/core/typing/callable_signature.py +22 -19
- pyglove/core/typing/callable_signature_test.py +3 -5
- pyglove/core/typing/class_schema.py +93 -54
- pyglove/core/typing/class_schema_test.py +4 -5
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/key_specs.py +5 -7
- pyglove/core/typing/key_specs_test.py +4 -4
- pyglove/core/typing/type_conversion.py +4 -5
- pyglove/core/typing/type_conversion_test.py +12 -12
- pyglove/core/typing/typed_missing.py +6 -7
- pyglove/core/typing/typed_missing_test.py +7 -8
- pyglove/core/typing/value_specs.py +287 -144
- pyglove/core/typing/value_specs_test.py +148 -25
- pyglove/core/utils/__init__.py +172 -0
- pyglove/core/{object_utils → utils}/common_traits.py +2 -2
- pyglove/core/{object_utils → utils}/common_traits_test.py +1 -3
- pyglove/core/utils/contextual.py +147 -0
- pyglove/core/utils/contextual_test.py +88 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +1 -3
- pyglove/core/{object_utils → utils}/error_utils.py +3 -3
- pyglove/core/{object_utils → utils}/error_utils_test.py +1 -1
- pyglove/core/{object_utils → utils}/formatting.py +1 -1
- pyglove/core/{object_utils → utils}/formatting_test.py +1 -2
- pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
- pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
- pyglove/core/{object_utils → utils}/json_conversion.py +1 -1
- pyglove/core/{object_utils → utils}/json_conversion_test.py +1 -3
- pyglove/core/{object_utils → utils}/missing.py +2 -2
- pyglove/core/{object_utils → utils}/missing_test.py +2 -4
- pyglove/core/utils/text_color.py +128 -0
- pyglove/core/utils/text_color_test.py +94 -0
- pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
- pyglove/core/{object_utils → utils}/timing.py +21 -10
- pyglove/core/{object_utils → utils}/timing_test.py +14 -12
- pyglove/core/{object_utils → utils}/value_location.py +2 -2
- pyglove/core/{object_utils → utils}/value_location_test.py +2 -4
- pyglove/core/views/base.py +25 -29
- pyglove/core/views/html/base.py +15 -16
- pyglove/core/views/html/controls/base.py +46 -9
- pyglove/core/views/html/controls/label.py +13 -2
- pyglove/core/views/html/controls/label_test.py +27 -8
- pyglove/core/views/html/controls/progress_bar.py +3 -5
- pyglove/core/views/html/controls/progress_bar_test.py +2 -2
- pyglove/core/views/html/controls/tab.py +217 -66
- pyglove/core/views/html/controls/tab_test.py +46 -15
- pyglove/core/views/html/tree_view.py +39 -37
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/METADATA +17 -3
- pyglove-0.4.5.dev202501250807.dist-info/RECORD +218 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -164
- pyglove-0.4.5.dev202411132359.dist-info/RECORD +0 -203
- /pyglove/core/{object_utils → utils}/docstr_utils.py +0 -0
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev202411132359.dist-info → pyglove-0.4.5.dev202501250807.dist-info}/top_level.txt +0 -0
@@ -11,15 +11,13 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
"""Tests for pyglove.symbolic.base."""
|
15
|
-
|
16
14
|
import copy
|
17
15
|
import inspect
|
18
16
|
from typing import Any
|
19
17
|
import unittest
|
20
18
|
|
21
|
-
from pyglove.core import object_utils
|
22
19
|
from pyglove.core import typing as pg_typing
|
20
|
+
from pyglove.core import utils
|
23
21
|
from pyglove.core import views
|
24
22
|
from pyglove.core.symbolic import base
|
25
23
|
from pyglove.core.symbolic.dict import Dict
|
@@ -33,7 +31,7 @@ class FieldUpdateTest(unittest.TestCase):
|
|
33
31
|
def test_basics(self):
|
34
32
|
x = Dict(x=1)
|
35
33
|
f = pg_typing.Field('x', pg_typing.Int())
|
36
|
-
update = base.FieldUpdate(
|
34
|
+
update = base.FieldUpdate(utils.KeyPath('x'), x, f, 1, 2)
|
37
35
|
self.assertEqual(update.path, 'x')
|
38
36
|
self.assertIs(update.target, x)
|
39
37
|
self.assertIs(update.field, f)
|
@@ -42,15 +40,15 @@ class FieldUpdateTest(unittest.TestCase):
|
|
42
40
|
|
43
41
|
def test_format(self):
|
44
42
|
self.assertEqual(
|
45
|
-
base.FieldUpdate(
|
46
|
-
|
47
|
-
)
|
43
|
+
base.FieldUpdate(utils.KeyPath('x'), Dict(x=1), None, 1, 2).format(
|
44
|
+
compact=True
|
45
|
+
),
|
48
46
|
'FieldUpdate(parent_path=, path=x, old_value=1, new_value=2)',
|
49
47
|
)
|
50
48
|
|
51
49
|
self.assertEqual(
|
52
50
|
base.FieldUpdate(
|
53
|
-
|
51
|
+
utils.KeyPath('a'), Dict(x=Dict(a=1)).x, None, 1, 2
|
54
52
|
).format(compact=True),
|
55
53
|
'FieldUpdate(parent_path=x, path=a, old_value=1, new_value=2)',
|
56
54
|
)
|
@@ -59,34 +57,34 @@ class FieldUpdateTest(unittest.TestCase):
|
|
59
57
|
x = Dict()
|
60
58
|
f = pg_typing.Field('x', pg_typing.Int())
|
61
59
|
self.assertEqual(
|
62
|
-
base.FieldUpdate(
|
63
|
-
base.FieldUpdate(
|
60
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 2),
|
61
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 2),
|
64
62
|
)
|
65
63
|
|
66
64
|
# Targets are not the same instance.
|
67
65
|
self.assertNotEqual(
|
68
|
-
base.FieldUpdate(
|
69
|
-
base.FieldUpdate(
|
66
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 2),
|
67
|
+
base.FieldUpdate(utils.KeyPath('a'), Dict(), f, 1, 2),
|
70
68
|
)
|
71
69
|
|
72
70
|
# Fields are not the same instance.
|
73
71
|
self.assertNotEqual(
|
74
|
-
base.FieldUpdate(
|
75
|
-
base.FieldUpdate(
|
72
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 2),
|
73
|
+
base.FieldUpdate(utils.KeyPath('b'), x, copy.copy(f), 1, 2),
|
76
74
|
)
|
77
75
|
|
78
76
|
self.assertNotEqual(
|
79
|
-
base.FieldUpdate(
|
80
|
-
base.FieldUpdate(
|
77
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 2),
|
78
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 0, 2),
|
81
79
|
)
|
82
80
|
|
83
81
|
self.assertNotEqual(
|
84
|
-
base.FieldUpdate(
|
85
|
-
base.FieldUpdate(
|
82
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 2),
|
83
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 1),
|
86
84
|
)
|
87
85
|
|
88
86
|
self.assertNotEqual(
|
89
|
-
base.FieldUpdate(
|
87
|
+
base.FieldUpdate(utils.KeyPath('a'), x, f, 1, 2), Dict()
|
90
88
|
)
|
91
89
|
|
92
90
|
|
@@ -15,11 +15,10 @@
|
|
15
15
|
|
16
16
|
import copy
|
17
17
|
import inspect
|
18
|
-
|
19
18
|
from typing import Any, List, Optional, Type
|
20
19
|
|
21
|
-
from pyglove.core import object_utils
|
22
20
|
from pyglove.core import typing as pg_typing
|
21
|
+
from pyglove.core import utils
|
23
22
|
from pyglove.core.symbolic import flags
|
24
23
|
from pyglove.core.symbolic import object as pg_object
|
25
24
|
|
@@ -129,9 +128,9 @@ def boilerplate_class(
|
|
129
128
|
cls.auto_register = True
|
130
129
|
|
131
130
|
allow_partial = value.allow_partial
|
132
|
-
def _freeze_field(
|
133
|
-
|
134
|
-
|
131
|
+
def _freeze_field(
|
132
|
+
path: utils.KeyPath, field: pg_typing.Field, value: Any
|
133
|
+
) -> Any:
|
135
134
|
# We do not do validation since Object is already in valid form.
|
136
135
|
del path
|
137
136
|
if not isinstance(field.key, pg_typing.ListKey):
|
@@ -26,8 +26,8 @@ import types
|
|
26
26
|
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
|
27
27
|
|
28
28
|
from pyglove.core import detouring
|
29
|
-
from pyglove.core import object_utils
|
30
29
|
from pyglove.core import typing as pg_typing
|
30
|
+
from pyglove.core import utils
|
31
31
|
|
32
32
|
from pyglove.core.symbolic import dict as pg_dict # pylint: disable=unused-import
|
33
33
|
from pyglove.core.symbolic import list as pg_list # pylint: disable=unused-import
|
@@ -45,11 +45,7 @@ class ClassWrapperMeta(pg_object.ObjectMeta):
|
|
45
45
|
|
46
46
|
def __getattr__(self, name):
|
47
47
|
"""Pass through attribute requests to sym_wrapped_cls."""
|
48
|
-
|
49
|
-
return super().__getattr__(name)
|
50
|
-
except AttributeError:
|
51
|
-
wrapped_cls = object.__getattribute__(self, 'sym_wrapped_cls')
|
52
|
-
return getattr(wrapped_cls, name)
|
48
|
+
return getattr(object.__getattribute__(self, 'sym_wrapped_cls'), name)
|
53
49
|
|
54
50
|
|
55
51
|
class ClassWrapper(pg_object.Object, metaclass=ClassWrapperMeta):
|
@@ -75,7 +71,7 @@ class _SubclassedWrapperBase(ClassWrapper):
|
|
75
71
|
# the `__init__` method.
|
76
72
|
auto_typing = False
|
77
73
|
|
78
|
-
@
|
74
|
+
@utils.explicit_method_override
|
79
75
|
def __init__(self, *args, **kwargs):
|
80
76
|
"""Overridden __init__ to construct symbolic wrapper only."""
|
81
77
|
# NOTE(daiyip): We avoid `__init__` to be called multiple times.
|
@@ -104,7 +100,7 @@ class _SubclassedWrapperBase(ClassWrapper):
|
|
104
100
|
def __init_subclass__(cls):
|
105
101
|
# Class wrappers inherit `__init__` from the user class. Therefore, we mark
|
106
102
|
# all of them as explicitly overridden.
|
107
|
-
|
103
|
+
utils.explicit_method_override(cls.__init__)
|
108
104
|
|
109
105
|
super().__init_subclass__()
|
110
106
|
if cls.__init__ is _SubclassedWrapperBase.__init__:
|
@@ -133,7 +129,7 @@ class _SubclassedWrapperBase(ClassWrapper):
|
|
133
129
|
init_arg_list, arg_fields = _extract_init_signature(
|
134
130
|
cls, auto_doc=cls.auto_doc, auto_typing=cls.auto_typing)
|
135
131
|
|
136
|
-
@
|
132
|
+
@utils.explicit_method_override
|
137
133
|
@functools.wraps(cls.__init__)
|
138
134
|
def _sym_init(self, *args, **kwargs):
|
139
135
|
_SubclassedWrapperBase.__init__(self, *args, **kwargs)
|
@@ -526,7 +522,7 @@ def apply_wrappers(
|
|
526
522
|
"""
|
527
523
|
if not wrapper_classes:
|
528
524
|
wrapper_classes = []
|
529
|
-
for _, c in
|
525
|
+
for _, c in utils.JSONConvertible.registered_types():
|
530
526
|
if (issubclass(c, ClassWrapper)
|
531
527
|
and c not in (ClassWrapper, _SubclassedWrapperBase)
|
532
528
|
and (not where or where(c))
|
@@ -548,13 +544,13 @@ def _extract_init_signature(
|
|
548
544
|
# Read args docstr from both class doc string and __init__ doc string.
|
549
545
|
args_docstr = dict()
|
550
546
|
if cls.__doc__:
|
551
|
-
cls_docstr =
|
547
|
+
cls_docstr = utils.DocStr.parse(cls.__doc__)
|
552
548
|
args_docstr = cls_docstr.args
|
553
549
|
if init_method.__doc__:
|
554
|
-
init_docstr =
|
550
|
+
init_docstr = utils.DocStr.parse(init_method.__doc__)
|
555
551
|
args_docstr.update(init_docstr.args)
|
556
|
-
docstr =
|
557
|
-
|
552
|
+
docstr = utils.DocStr(
|
553
|
+
utils.DocStrStyle.GOOGLE,
|
558
554
|
short_description=None,
|
559
555
|
long_description=None,
|
560
556
|
examples=[],
|
@@ -588,7 +588,7 @@ class ClassWrapperTest(unittest.TestCase):
|
|
588
588
|
self.assertIsInstance(C(1, 2), ClassWrapper)
|
589
589
|
self.assertTrue(pg_eq(C(1, 2), C(1, 2)))
|
590
590
|
self.assertEqual(list(C.__schema__.fields.keys()), ['x', 'y'])
|
591
|
-
self.assertEqual(repr(C), f'<class {C.
|
591
|
+
self.assertEqual(repr(C), f'<class {C.__type_name__!r}>')
|
592
592
|
|
593
593
|
def test_custom_metaclass(self):
|
594
594
|
|
@@ -604,7 +604,7 @@ class ClassWrapperTest(unittest.TestCase):
|
|
604
604
|
A1 = pg_wrap(A) # pylint: disable=invalid-name
|
605
605
|
self.assertTrue(issubclass(A1, ClassWrapper))
|
606
606
|
self.assertTrue(issubclass(A1, A))
|
607
|
-
self.assertEqual(A1.
|
607
|
+
self.assertEqual(A1.__type_name__, 'pyglove.core.symbolic.class_wrapper_test.A')
|
608
608
|
self.assertEqual(A1.__schema__, pg_typing.Schema([]))
|
609
609
|
self.assertEqual(A1.foo, 'foo')
|
610
610
|
self.assertRegex(repr(A1), r'Symbolic\[.*\]')
|
@@ -19,7 +19,7 @@ import sys
|
|
19
19
|
import types
|
20
20
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
21
21
|
|
22
|
-
from pyglove.core import
|
22
|
+
from pyglove.core import utils
|
23
23
|
from pyglove.core.symbolic.base import Symbolic
|
24
24
|
from pyglove.core.symbolic.object import Object
|
25
25
|
import pyglove.core.typing as pg_typing
|
@@ -39,7 +39,7 @@ class Compound(Object):
|
|
39
39
|
# from the user class to compound with.
|
40
40
|
Object.__init_subclass__(cls)
|
41
41
|
|
42
|
-
@
|
42
|
+
@utils.explicit_method_override
|
43
43
|
def __init__(self, *args, **kwargs):
|
44
44
|
# `explicit_init` allows the `__init__` of the other classes that sit after
|
45
45
|
# `Compound` to be bypassed.
|
@@ -11,15 +11,13 @@
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
|
-
"""Tests for pyglove.compounding."""
|
15
|
-
|
16
14
|
import abc
|
17
15
|
import dataclasses
|
18
16
|
import sys
|
19
17
|
import unittest
|
20
18
|
|
21
|
-
from pyglove.core import object_utils
|
22
19
|
from pyglove.core import typing as pg_typing
|
20
|
+
from pyglove.core import utils
|
23
21
|
from pyglove.core.symbolic.compounding import compound as pg_compound
|
24
22
|
from pyglove.core.symbolic.compounding import compound_class as pg_compound_class
|
25
23
|
from pyglove.core.symbolic.dict import Dict
|
@@ -145,7 +143,7 @@ class UserClassTest(unittest.TestCase):
|
|
145
143
|
class A(Object):
|
146
144
|
x: int
|
147
145
|
|
148
|
-
@
|
146
|
+
@utils.explicit_method_override
|
149
147
|
def __init__(self, x):
|
150
148
|
super().__init__(x=x)
|
151
149
|
assert type(self) is A # pylint: disable=unidiomatic-typecheck
|
@@ -0,0 +1,288 @@
|
|
1
|
+
# Copyright 2025 The PyGlove Authors
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
"""Contextual objects.
|
15
|
+
|
16
|
+
This module provides support for defining and working with `ContextualObject`s,
|
17
|
+
whose attributes can dynamically adapt based on context managers and parent
|
18
|
+
objects.
|
19
|
+
|
20
|
+
# Overview:
|
21
|
+
|
22
|
+
Contextual objects are specialized objects whose attribute values can be:
|
23
|
+
1. Dynamically overridden using the `pg.contextual_override` context manager.
|
24
|
+
2. Accessed via parent objects using the `pg.contextual_attribute` placeholder.
|
25
|
+
|
26
|
+
This flexibility is particularly useful in scenarios where attributes need
|
27
|
+
to respond to dynamic runtime conditions or inherit behavior from hierarchical
|
28
|
+
structures.
|
29
|
+
|
30
|
+
Example:
|
31
|
+
|
32
|
+
```python
|
33
|
+
class A(pg.ContextualObject):
|
34
|
+
x: int
|
35
|
+
y: Any = pg.contextual_attribute()
|
36
|
+
|
37
|
+
a = A(1)
|
38
|
+
print(a.x) # 1
|
39
|
+
with pg.contextual_override(x=2):
|
40
|
+
print(a.x) # 2
|
41
|
+
print(a.x) #1
|
42
|
+
|
43
|
+
pg.Dict(y=3, a=a)
|
44
|
+
print(a.y) # 3. (Accessing parent's y)
|
45
|
+
```
|
46
|
+
"""
|
47
|
+
|
48
|
+
import threading
|
49
|
+
from typing import Annotated, Any, ContextManager, Dict, Optional, Type
|
50
|
+
from pyglove.core import utils as pg_utils
|
51
|
+
from pyglove.core.symbolic import base
|
52
|
+
from pyglove.core.symbolic import inferred as pg_inferred
|
53
|
+
from pyglove.core.symbolic import object as pg_object
|
54
|
+
from pyglove.core.views.html import tree_view
|
55
|
+
|
56
|
+
|
57
|
+
class ContextualObject(pg_object.Object):
|
58
|
+
"""Base class for contextual objects.
|
59
|
+
|
60
|
+
Contextual objects are objects whose attributes can be dynamically overridden
|
61
|
+
using `pg.contextual_override` or resolved through
|
62
|
+
`pg.contextual_attribute`, allowing them to inherit values from their
|
63
|
+
containing objects.
|
64
|
+
|
65
|
+
Usages:
|
66
|
+
|
67
|
+
```
|
68
|
+
# Define a contextual object.
|
69
|
+
class A(pg.ContextualObject):
|
70
|
+
x: int
|
71
|
+
y: Any = pg.contextual_attribute()
|
72
|
+
|
73
|
+
# Create an instance of A
|
74
|
+
a = A(1)
|
75
|
+
print(a.x) # Outputs: 1
|
76
|
+
print(a.y) # Raises an error, as `a` has no containing object.
|
77
|
+
|
78
|
+
# Define another contextual object containing an instance of A
|
79
|
+
class B(pg.ContextualObject):
|
80
|
+
y: int
|
81
|
+
a: A
|
82
|
+
|
83
|
+
# Create an instance of B, containing "a"
|
84
|
+
b = B(y=2, a=a)
|
85
|
+
print(a.y) # Outputs: 2, as "y" is resolved from the containing object (B).
|
86
|
+
|
87
|
+
# Contextual overrides are thread-specific
|
88
|
+
with pg.contextual_override(x=2):
|
89
|
+
print(a.x) # Outputs: 2
|
90
|
+
|
91
|
+
# Thread-specific behavior of `pg.contextual_override`
|
92
|
+
def foo(a):
|
93
|
+
print(a.x)
|
94
|
+
|
95
|
+
with pg.contextual_override(x=3):
|
96
|
+
t = threading.Thread(target=foo, args=(a,))
|
97
|
+
t.start()
|
98
|
+
t.join()
|
99
|
+
# Outputs: 1, because `pg.contextual_override` is limited to the current
|
100
|
+
# thread to avoid clashes in multi-threaded environments.
|
101
|
+
|
102
|
+
# To propagate the override to a new thread, use `pg.with_contextual_override`
|
103
|
+
with pg.contextual_override(x=3):
|
104
|
+
t = threading.Thread(target=pg.with_contextual_override(foo), args=(a,))
|
105
|
+
t.start()
|
106
|
+
t.join()
|
107
|
+
# Outputs: 3, as the override is explicitly propagated.
|
108
|
+
```
|
109
|
+
"""
|
110
|
+
|
111
|
+
# Override __repr__ format to use inferred values when available.
|
112
|
+
__repr_format_kwargs__ = dict(
|
113
|
+
compact=True,
|
114
|
+
use_inferred=True,
|
115
|
+
)
|
116
|
+
|
117
|
+
# Override __str__ format to use inferred values when available.
|
118
|
+
__str_format_kwargs__ = dict(
|
119
|
+
compact=False,
|
120
|
+
verbose=False,
|
121
|
+
use_inferred=True,
|
122
|
+
)
|
123
|
+
|
124
|
+
def _on_bound(self):
|
125
|
+
super()._on_bound()
|
126
|
+
self._contextual_overrides = threading.local()
|
127
|
+
|
128
|
+
def _sym_inferred(self, key: str, **kwargs):
|
129
|
+
"""Override to allow attribute to access scoped value.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
key: attribute name.
|
133
|
+
**kwargs: Optional keyword arguments for value inference.
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
The value of the symbolic attribute. If not available, returns the
|
137
|
+
default value.
|
138
|
+
|
139
|
+
Raises:
|
140
|
+
AttributeError: If the attribute does not exist or contextual attribute
|
141
|
+
is not ready.
|
142
|
+
"""
|
143
|
+
if key not in self._sym_attributes:
|
144
|
+
raise AttributeError(key)
|
145
|
+
|
146
|
+
# Step 1: Try use value from `self.override`.
|
147
|
+
# The reason is that `self.override` is short-lived and explicitly specified
|
148
|
+
# by the user in scenarios like `LangFunc.render`, which should not be
|
149
|
+
# affected by `pg.contextual_override`.
|
150
|
+
v = pg_utils.contextual.get_scoped_value(self._contextual_overrides, key)
|
151
|
+
if v is not None:
|
152
|
+
return v.value
|
153
|
+
|
154
|
+
# Step 2: Try use value from `pg.contextual_override` with `override_attrs`.
|
155
|
+
# This gives users a chance to override the bound attributes of components
|
156
|
+
# from the top, allowing change of bindings without modifying the code
|
157
|
+
# that produces the components.
|
158
|
+
override = pg_utils.contextual.get_contextual_override(key)
|
159
|
+
if override and override.override_attrs:
|
160
|
+
return override.value
|
161
|
+
|
162
|
+
# Step 3: Try use value from the symbolic tree, starting from self to
|
163
|
+
# the root of the tree.
|
164
|
+
# Step 4: If the value is not present, use the value from `context()` (
|
165
|
+
# override_attrs=False).
|
166
|
+
# Step 5: Otherwise use the default value from `ContextualAttribute`.
|
167
|
+
return super()._sym_inferred(key, context_override=override, **kwargs)
|
168
|
+
|
169
|
+
def override(
|
170
|
+
self,
|
171
|
+
**kwargs
|
172
|
+
) -> ContextManager[Dict[str, pg_utils.contextual.ContextualOverride]]:
|
173
|
+
"""Context manager to override the attributes of this component."""
|
174
|
+
vs = {
|
175
|
+
k: pg_utils.contextual.ContextualOverride(v)
|
176
|
+
for k, v in kwargs.items()
|
177
|
+
}
|
178
|
+
return pg_utils.contextual.contextual_scope(
|
179
|
+
self._contextual_overrides, **vs
|
180
|
+
)
|
181
|
+
|
182
|
+
def __getattribute__(self, name: str) -> Any:
|
183
|
+
"""Override __getattribute__ to deal with class attribute override."""
|
184
|
+
if not name.startswith('_') and hasattr(self.__class__, name):
|
185
|
+
tls = self.__dict__.get('_contextual_overrides', None)
|
186
|
+
if tls is not None:
|
187
|
+
v = pg_utils.contextual.get_scoped_value(tls, name)
|
188
|
+
if v is not None:
|
189
|
+
return v.value
|
190
|
+
return super().__getattribute__(name)
|
191
|
+
|
192
|
+
|
193
|
+
class ContextualAttribute(
|
194
|
+
pg_inferred.ValueFromParentChain, tree_view.HtmlTreeView.Extension
|
195
|
+
):
|
196
|
+
"""Attributes whose values are inferred from the containing objects."""
|
197
|
+
|
198
|
+
NO_DEFAULT = (pg_utils.MISSING_VALUE,)
|
199
|
+
|
200
|
+
type: Annotated[Optional[Type[Any]], 'An optional type constraint.'] = None
|
201
|
+
|
202
|
+
default: Any = NO_DEFAULT
|
203
|
+
|
204
|
+
def value_from(
|
205
|
+
self,
|
206
|
+
parent,
|
207
|
+
*,
|
208
|
+
context_override: Optional[pg_utils.contextual.ContextualOverride] = None,
|
209
|
+
**kwargs,
|
210
|
+
):
|
211
|
+
if (parent not in (None, self.sym_parent)
|
212
|
+
and isinstance(parent, ContextualObject)):
|
213
|
+
# Apply original search logic along the contextual object containing
|
214
|
+
# chain.
|
215
|
+
return super().value_from(parent, **kwargs)
|
216
|
+
elif parent is None:
|
217
|
+
# When there is no value inferred from the symbolic tree.
|
218
|
+
# Search context override, and then attribute-level default.
|
219
|
+
if context_override:
|
220
|
+
return context_override.value
|
221
|
+
if self.default == ContextualAttribute.NO_DEFAULT:
|
222
|
+
return pg_utils.MISSING_VALUE
|
223
|
+
return self.default
|
224
|
+
else:
|
225
|
+
return pg_utils.MISSING_VALUE
|
226
|
+
|
227
|
+
def _html_tree_view_content(
|
228
|
+
self,
|
229
|
+
*,
|
230
|
+
view: tree_view.HtmlTreeView,
|
231
|
+
parent: Any = None,
|
232
|
+
root_path: Optional[pg_utils.KeyPath] = None,
|
233
|
+
**kwargs,
|
234
|
+
) -> tree_view.Html:
|
235
|
+
inferred_value = pg_utils.MISSING_VALUE
|
236
|
+
if isinstance(parent, base.Symbolic) and root_path:
|
237
|
+
inferred_value = parent.sym_inferred(
|
238
|
+
root_path.key, pg_utils.MISSING_VALUE
|
239
|
+
)
|
240
|
+
|
241
|
+
if inferred_value is not pg_utils.MISSING_VALUE:
|
242
|
+
kwargs.pop('name', None)
|
243
|
+
return view.render(
|
244
|
+
inferred_value, parent=self,
|
245
|
+
root_path=pg_utils.KeyPath('<inferred>', root_path),
|
246
|
+
**view.get_passthrough_kwargs(**kwargs)
|
247
|
+
)
|
248
|
+
return tree_view.Html.element(
|
249
|
+
'div',
|
250
|
+
[
|
251
|
+
'(not available)',
|
252
|
+
],
|
253
|
+
css_classes=['unavailable-contextual'],
|
254
|
+
)
|
255
|
+
|
256
|
+
def _html_tree_view_config(self) -> Dict[str, Any]:
|
257
|
+
return tree_view.HtmlTreeView.get_kwargs(
|
258
|
+
super()._html_tree_view_config(),
|
259
|
+
dict(
|
260
|
+
collapse_level=1,
|
261
|
+
)
|
262
|
+
)
|
263
|
+
|
264
|
+
@classmethod
|
265
|
+
def _html_tree_view_css_styles(cls) -> list[str]:
|
266
|
+
return super()._html_tree_view_css_styles() + [
|
267
|
+
"""
|
268
|
+
.contextual-attribute {
|
269
|
+
color: purple;
|
270
|
+
}
|
271
|
+
.unavailable-contextual {
|
272
|
+
color: gray;
|
273
|
+
font-style: italic;
|
274
|
+
}
|
275
|
+
"""
|
276
|
+
]
|
277
|
+
|
278
|
+
|
279
|
+
# NOTE(daiyip): Returning Any instead of `pg.ContextualAttribute` to
|
280
|
+
# avoid pytype check error as `contextual_attribute()` can be assigned to any
|
281
|
+
# type.
|
282
|
+
def contextual_attribute(
|
283
|
+
type: Optional[Type[Any]] = None, # pylint: disable=redefined-builtin
|
284
|
+
default: Any = ContextualAttribute.NO_DEFAULT,
|
285
|
+
) -> Any:
|
286
|
+
"""Value marker for a contextual attribute."""
|
287
|
+
return ContextualAttribute(type=type, default=default, allow_partial=True)
|
288
|
+
|