pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501132210__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 +54 -20
- 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 +309 -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 +54 -41
- pyglove/core/geno/base_test.py +2 -4
- pyglove/core/geno/categorical.py +37 -28
- pyglove/core/geno/custom.py +19 -16
- pyglove/core/geno/numerical.py +20 -17
- pyglove/core/geno/space.py +4 -5
- pyglove/core/hyper/base.py +6 -6
- pyglove/core/hyper/categorical.py +94 -55
- 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 +2 -4
- pyglove/core/hyper/dynamic_evaluation.py +5 -6
- 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/io/__init__.py +1 -0
- pyglove/core/io/file_system.py +17 -7
- pyglove/core/io/file_system_test.py +2 -0
- pyglove/core/io/sequence.py +299 -0
- pyglove/core/io/sequence_test.py +124 -0
- 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 +17 -5
- pyglove/core/patching/rule_based_test.py +27 -4
- pyglove/core/symbolic/__init__.py +2 -7
- pyglove/core/symbolic/base.py +320 -183
- pyglove/core/symbolic/base_test.py +123 -19
- pyglove/core/symbolic/boilerplate.py +7 -13
- pyglove/core/symbolic/boilerplate_test.py +25 -23
- pyglove/core/symbolic/class_wrapper.py +48 -45
- pyglove/core/symbolic/class_wrapper_test.py +2 -2
- pyglove/core/symbolic/compounding.py +9 -15
- pyglove/core/symbolic/compounding_test.py +2 -4
- pyglove/core/symbolic/dict.py +154 -110
- pyglove/core/symbolic/dict_test.py +238 -130
- pyglove/core/symbolic/diff.py +199 -10
- pyglove/core/symbolic/diff_test.py +226 -0
- pyglove/core/symbolic/flags.py +1 -1
- pyglove/core/symbolic/functor.py +29 -26
- pyglove/core/symbolic/functor_test.py +102 -50
- pyglove/core/symbolic/inferred.py +2 -2
- pyglove/core/symbolic/list.py +81 -50
- pyglove/core/symbolic/list_test.py +119 -97
- pyglove/core/symbolic/object.py +225 -113
- pyglove/core/symbolic/object_test.py +320 -108
- pyglove/core/symbolic/origin.py +17 -14
- pyglove/core/symbolic/origin_test.py +4 -2
- pyglove/core/symbolic/pure_symbolic.py +4 -3
- pyglove/core/symbolic/ref.py +108 -21
- pyglove/core/symbolic/ref_test.py +93 -0
- pyglove/core/symbolic/symbolize_test.py +10 -2
- pyglove/core/tuning/local_backend.py +2 -2
- pyglove/core/tuning/protocols.py +3 -3
- pyglove/core/tuning/sample_test.py +3 -3
- pyglove/core/typing/__init__.py +14 -5
- pyglove/core/typing/annotation_conversion.py +43 -27
- pyglove/core/typing/annotation_conversion_test.py +23 -0
- pyglove/core/typing/callable_ext.py +241 -3
- pyglove/core/typing/callable_ext_test.py +255 -0
- pyglove/core/typing/callable_signature.py +510 -66
- pyglove/core/typing/callable_signature_test.py +619 -99
- pyglove/core/typing/class_schema.py +229 -154
- pyglove/core/typing/class_schema_test.py +149 -95
- pyglove/core/typing/custom_typing.py +5 -4
- pyglove/core/typing/inspect.py +63 -0
- pyglove/core/typing/inspect_test.py +39 -0
- pyglove/core/typing/key_specs.py +10 -11
- pyglove/core/typing/key_specs_test.py +7 -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 +604 -362
- pyglove/core/typing/value_specs_test.py +328 -90
- pyglove/core/utils/__init__.py +164 -0
- pyglove/core/{object_utils → utils}/common_traits.py +3 -67
- pyglove/core/utils/common_traits_test.py +36 -0
- pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
- pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
- pyglove/core/{object_utils → utils}/error_utils.py +78 -9
- pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
- pyglove/core/utils/formatting.py +464 -0
- pyglove/core/utils/formatting_test.py +453 -0
- 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 +177 -52
- pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
- pyglove/core/{object_utils → utils}/missing.py +3 -3
- 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/utils/timing.py +236 -0
- pyglove/core/utils/timing_test.py +154 -0
- pyglove/core/{object_utils → utils}/value_location.py +275 -6
- pyglove/core/utils/value_location_test.py +707 -0
- pyglove/core/views/__init__.py +32 -0
- pyglove/core/views/base.py +804 -0
- pyglove/core/views/base_test.py +580 -0
- pyglove/core/views/html/__init__.py +27 -0
- pyglove/core/views/html/base.py +547 -0
- pyglove/core/views/html/base_test.py +830 -0
- pyglove/core/views/html/controls/__init__.py +35 -0
- pyglove/core/views/html/controls/base.py +275 -0
- pyglove/core/views/html/controls/label.py +207 -0
- pyglove/core/views/html/controls/label_test.py +157 -0
- pyglove/core/views/html/controls/progress_bar.py +183 -0
- pyglove/core/views/html/controls/progress_bar_test.py +97 -0
- pyglove/core/views/html/controls/tab.py +320 -0
- pyglove/core/views/html/controls/tab_test.py +87 -0
- pyglove/core/views/html/controls/tooltip.py +99 -0
- pyglove/core/views/html/controls/tooltip_test.py +99 -0
- pyglove/core/views/html/tree_view.py +1517 -0
- pyglove/core/views/html/tree_view_test.py +1461 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/METADATA +18 -4
- pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/WHEEL +1 -1
- pyglove/core/object_utils/__init__.py +0 -154
- pyglove/core/object_utils/common_traits_test.py +0 -82
- pyglove/core/object_utils/formatting.py +0 -234
- pyglove/core/object_utils/formatting_test.py +0 -223
- pyglove/core/object_utils/value_location_test.py +0 -385
- pyglove/core/symbolic/schema_utils.py +0 -327
- pyglove/core/symbolic/schema_utils_test.py +0 -57
- pyglove/core/typing/class_schema_utils.py +0 -202
- pyglove/core/typing/class_schema_utils_test.py +0 -194
- pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
- /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/LICENSE +0 -0
- {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,94 @@
|
|
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
|
+
import inspect
|
15
|
+
import os
|
16
|
+
import unittest
|
17
|
+
from pyglove.core.utils import text_color
|
18
|
+
|
19
|
+
|
20
|
+
class TextColorTest(unittest.TestCase):
|
21
|
+
|
22
|
+
def setUp(self):
|
23
|
+
super().setUp()
|
24
|
+
os.environ.pop('ANSI_COLORS_DISABLED', None)
|
25
|
+
os.environ.pop('NO_COLOR', None)
|
26
|
+
os.environ['FORCE_COLOR'] = '1'
|
27
|
+
|
28
|
+
def test_colored_block_without_colors_and_styles(self):
|
29
|
+
self.assertEqual(text_color.colored_block('foo', '{{', '}}'), 'foo')
|
30
|
+
|
31
|
+
def test_colored_block(self):
|
32
|
+
original_text = inspect.cleandoc("""
|
33
|
+
Hi << foo >>
|
34
|
+
<# print x if x is present #>
|
35
|
+
<% if x %>
|
36
|
+
<< x >>
|
37
|
+
<% endif %>
|
38
|
+
""")
|
39
|
+
|
40
|
+
colored_text = text_color.colored_block(
|
41
|
+
text_color.colored(original_text, color='blue'),
|
42
|
+
'<<', '>>',
|
43
|
+
color='white',
|
44
|
+
background='blue',
|
45
|
+
)
|
46
|
+
origin_color = '\x1b[34m'
|
47
|
+
reset = '\x1b[0m'
|
48
|
+
block_color = text_color.colored(
|
49
|
+
'TEXT', color='white', background='blue'
|
50
|
+
).split('TEXT')[0]
|
51
|
+
self.assertEqual(
|
52
|
+
colored_text,
|
53
|
+
f'{origin_color}Hi {block_color}<< foo >>{reset}{origin_color}\n'
|
54
|
+
'<# print x if x is present #>\n<% if x %>\n'
|
55
|
+
f'{block_color}<< x >>{reset}{origin_color}\n'
|
56
|
+
f'<% endif %>{reset}'
|
57
|
+
)
|
58
|
+
self.assertEqual(text_color.decolor(colored_text), original_text)
|
59
|
+
|
60
|
+
def test_colored_block_without_full_match(self):
|
61
|
+
self.assertEqual(
|
62
|
+
text_color.colored_block(
|
63
|
+
'Hi {{ foo',
|
64
|
+
'{{', '}}',
|
65
|
+
color='white',
|
66
|
+
background='blue',
|
67
|
+
),
|
68
|
+
'Hi {{ foo'
|
69
|
+
)
|
70
|
+
|
71
|
+
def test_colored_block_without_termcolor(self):
|
72
|
+
termcolor = text_color.termcolor
|
73
|
+
text_color.termcolor = None
|
74
|
+
original_text = inspect.cleandoc("""
|
75
|
+
Hi {{ foo }}
|
76
|
+
{# print x if x is present #}
|
77
|
+
{% if x %}
|
78
|
+
{{ x }}
|
79
|
+
{% endif %}
|
80
|
+
""")
|
81
|
+
|
82
|
+
colored_text = text_color.colored_block(
|
83
|
+
text_color.colored(original_text, color='blue'),
|
84
|
+
'{{', '}}',
|
85
|
+
color='white',
|
86
|
+
background='blue',
|
87
|
+
)
|
88
|
+
self.assertEqual(colored_text, original_text)
|
89
|
+
self.assertEqual(text_color.decolor(colored_text), original_text)
|
90
|
+
text_color.termcolor = termcolor
|
91
|
+
|
92
|
+
|
93
|
+
if __name__ == '__main__':
|
94
|
+
unittest.main()
|
@@ -11,13 +11,11 @@
|
|
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.object_utils.thread_local."""
|
15
|
-
|
16
14
|
import threading
|
17
15
|
import time
|
18
16
|
import unittest
|
19
17
|
|
20
|
-
from pyglove.core.
|
18
|
+
from pyglove.core.utils import thread_local
|
21
19
|
|
22
20
|
|
23
21
|
class ThreadLocalTest(unittest.TestCase):
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# Copyright 2024 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
|
+
"""Utilities for timing."""
|
15
|
+
|
16
|
+
import collections
|
17
|
+
import dataclasses
|
18
|
+
import time
|
19
|
+
from typing import Any, Dict, List, Optional
|
20
|
+
|
21
|
+
from pyglove.core.utils import error_utils
|
22
|
+
from pyglove.core.utils import json_conversion
|
23
|
+
from pyglove.core.utils import thread_local
|
24
|
+
|
25
|
+
|
26
|
+
class TimeIt:
|
27
|
+
"""Context manager for timing the execution of a code block."""
|
28
|
+
|
29
|
+
@dataclasses.dataclass(frozen=True)
|
30
|
+
class Status(json_conversion.JSONConvertible):
|
31
|
+
"""Status of a single `pg.timeit`."""
|
32
|
+
name: str
|
33
|
+
elapse: float = 0.0
|
34
|
+
has_ended: bool = True
|
35
|
+
error: Optional[error_utils.ErrorInfo] = None
|
36
|
+
|
37
|
+
@property
|
38
|
+
def has_started(self) -> bool:
|
39
|
+
"""Returns whether the context has started."""
|
40
|
+
return self.elapse > 0
|
41
|
+
|
42
|
+
@property
|
43
|
+
def has_error(self) -> bool:
|
44
|
+
"""Returns whether the context has error."""
|
45
|
+
return self.error is not None
|
46
|
+
|
47
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
48
|
+
return self.to_json_dict(
|
49
|
+
fields=dict(
|
50
|
+
name=(self.name, None),
|
51
|
+
elapse=(self.elapse, 0.0),
|
52
|
+
has_ended=(self.has_ended, True),
|
53
|
+
error=(self.error, None),
|
54
|
+
),
|
55
|
+
exclude_default=True,
|
56
|
+
**kwargs,
|
57
|
+
)
|
58
|
+
|
59
|
+
@dataclasses.dataclass
|
60
|
+
class StatusSummary(json_conversion.JSONConvertible):
|
61
|
+
"""Aggregated summary for repeated calls for `pg.timeit`."""
|
62
|
+
|
63
|
+
@dataclasses.dataclass
|
64
|
+
class Entry(json_conversion.JSONConvertible):
|
65
|
+
"""Aggregated status from the `pg.timeit` calls of the same name."""
|
66
|
+
|
67
|
+
num_started: int = 0
|
68
|
+
num_ended: int = 0
|
69
|
+
num_failed: int = 0
|
70
|
+
avg_duration: float = 0.0
|
71
|
+
error_tags: Dict[str, int] = dataclasses.field(
|
72
|
+
default_factory=lambda: collections.defaultdict(int)
|
73
|
+
)
|
74
|
+
|
75
|
+
def update(self, status: 'TimeIt.Status'):
|
76
|
+
self.avg_duration = (
|
77
|
+
(self.avg_duration * self.num_started + status.elapse)
|
78
|
+
/ (self.num_started + 1)
|
79
|
+
)
|
80
|
+
self.num_started += 1
|
81
|
+
if status.has_ended:
|
82
|
+
self.num_ended += 1
|
83
|
+
if status.has_error:
|
84
|
+
self.num_failed += 1
|
85
|
+
assert status.error is not None
|
86
|
+
self.error_tags[status.error.tag] += 1
|
87
|
+
|
88
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
89
|
+
return self.to_json_dict(
|
90
|
+
fields=dict(
|
91
|
+
num_started=(self.num_started, 0),
|
92
|
+
num_ended=(self.num_ended, 0),
|
93
|
+
num_failed=(self.num_failed, 0),
|
94
|
+
avg_duration=(self.avg_duration, 0.0),
|
95
|
+
error_tags=(self.error_tags, {}),
|
96
|
+
),
|
97
|
+
exclude_default=True,
|
98
|
+
**kwargs,
|
99
|
+
)
|
100
|
+
|
101
|
+
breakdown: dict[str, 'TimeIt.StatusSummary.Entry'] = (
|
102
|
+
dataclasses.field(default_factory=dict)
|
103
|
+
)
|
104
|
+
|
105
|
+
def __bool__(self) -> bool:
|
106
|
+
"""Returns True if the summary is non-empty."""
|
107
|
+
return bool(self.breakdown)
|
108
|
+
|
109
|
+
def aggregate(self, timeit_status: Dict[str, 'TimeIt.Status']):
|
110
|
+
for k, v in timeit_status.items():
|
111
|
+
if k not in self.breakdown:
|
112
|
+
self.breakdown[k] = TimeIt.StatusSummary.Entry()
|
113
|
+
self.breakdown[k].update(v)
|
114
|
+
|
115
|
+
def to_json(self, **kwargs) -> Dict[str, Any]:
|
116
|
+
return self.to_json_dict(
|
117
|
+
fields=dict(
|
118
|
+
breakdown=(self.breakdown, {}),
|
119
|
+
),
|
120
|
+
exclude_default=True,
|
121
|
+
**kwargs,
|
122
|
+
)
|
123
|
+
|
124
|
+
def __init__(self, name: str = ''):
|
125
|
+
self._name: str = name
|
126
|
+
self._start_time: Optional[float] = None
|
127
|
+
self._end_time: Optional[float] = None
|
128
|
+
self._child_contexts: Dict[str, TimeIt] = {}
|
129
|
+
self._error: Optional[error_utils.ErrorInfo] = None
|
130
|
+
self._parent: Optional[TimeIt] = None
|
131
|
+
|
132
|
+
@property
|
133
|
+
def name(self) -> str:
|
134
|
+
"""Returns the name of the context."""
|
135
|
+
return self._name
|
136
|
+
|
137
|
+
@property
|
138
|
+
def children(self) -> List['TimeIt']:
|
139
|
+
"""Returns child contexts."""
|
140
|
+
return list(self._child_contexts.values())
|
141
|
+
|
142
|
+
def add(self, context: 'TimeIt'):
|
143
|
+
"""Adds a child context."""
|
144
|
+
if context.name in self._child_contexts:
|
145
|
+
raise ValueError(f'`timeit` with name {context.name!r} already exists.')
|
146
|
+
self._child_contexts[context.name] = context
|
147
|
+
|
148
|
+
def start(self):
|
149
|
+
"""Starts timing."""
|
150
|
+
self._start_time = time.time()
|
151
|
+
|
152
|
+
def end(self, error: Optional[BaseException] = None) -> bool:
|
153
|
+
"""Ends timing."""
|
154
|
+
if not self.has_ended:
|
155
|
+
self._end_time = time.time()
|
156
|
+
self._error = (
|
157
|
+
None if error is None else error_utils.ErrorInfo.from_exception(error)
|
158
|
+
)
|
159
|
+
return True
|
160
|
+
return False
|
161
|
+
|
162
|
+
@property
|
163
|
+
def has_started(self) -> bool:
|
164
|
+
"""Returns whether the context has started."""
|
165
|
+
return self._start_time is not None
|
166
|
+
|
167
|
+
@property
|
168
|
+
def has_ended(self) -> bool:
|
169
|
+
"""Returns whether the context has ended."""
|
170
|
+
return self._end_time is not None
|
171
|
+
|
172
|
+
@property
|
173
|
+
def start_time(self) -> Optional[float]:
|
174
|
+
"""Returns start time."""
|
175
|
+
return self._start_time
|
176
|
+
|
177
|
+
@property
|
178
|
+
def end_time(self) -> Optional[float]:
|
179
|
+
"""Returns end time."""
|
180
|
+
return self._end_time
|
181
|
+
|
182
|
+
@property
|
183
|
+
def error(self) -> Optional[error_utils.ErrorInfo]:
|
184
|
+
"""Returns error."""
|
185
|
+
return self._error
|
186
|
+
|
187
|
+
@property
|
188
|
+
def has_error(self) -> bool:
|
189
|
+
"""Returns whether the context has error."""
|
190
|
+
return self._error is not None
|
191
|
+
|
192
|
+
@property
|
193
|
+
def elapse(self) -> float:
|
194
|
+
"""Returns the elapse since start until end."""
|
195
|
+
if self._start_time is None:
|
196
|
+
return 0
|
197
|
+
if self._end_time is None:
|
198
|
+
return time.time() - self._start_time
|
199
|
+
return self._end_time - self._start_time # pytype: disable=unsupported-operands
|
200
|
+
|
201
|
+
def status(self) -> Dict[str, Status]:
|
202
|
+
"""Gets the status of all `timeit` under this context."""
|
203
|
+
result = {
|
204
|
+
self.name: TimeIt.Status(
|
205
|
+
name=self.name, elapse=self.elapse,
|
206
|
+
has_ended=self.has_ended, error=self._error,
|
207
|
+
)
|
208
|
+
}
|
209
|
+
for child in self._child_contexts.values():
|
210
|
+
child_result = child.status()
|
211
|
+
for k, v in child_result.items():
|
212
|
+
key = f'{self.name}.{k}' if self.name else k
|
213
|
+
result[key] = v
|
214
|
+
return result
|
215
|
+
|
216
|
+
def __enter__(self):
|
217
|
+
parent = thread_local.thread_local_get('__timing_context__', None)
|
218
|
+
if parent is not None:
|
219
|
+
parent.add(self)
|
220
|
+
self._parent = parent
|
221
|
+
thread_local.thread_local_set('__timing_context__', self)
|
222
|
+
self.start()
|
223
|
+
return self
|
224
|
+
|
225
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
226
|
+
del exc_type, traceback
|
227
|
+
self.end(exc_value)
|
228
|
+
if self._parent is None:
|
229
|
+
thread_local.thread_local_del('__timing_context__')
|
230
|
+
else:
|
231
|
+
thread_local.thread_local_set('__timing_context__', self._parent)
|
232
|
+
|
233
|
+
|
234
|
+
def timeit(name: str = '') -> TimeIt:
|
235
|
+
"""Context manager to time a block of code."""
|
236
|
+
return TimeIt(name)
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# Copyright 2024 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
|
+
import time
|
15
|
+
import unittest
|
16
|
+
|
17
|
+
from pyglove.core.utils import json_conversion
|
18
|
+
from pyglove.core.utils import timing
|
19
|
+
|
20
|
+
|
21
|
+
class TimeItTest(unittest.TestCase):
|
22
|
+
"""Tests for `pg.symbolic.thread_local`."""
|
23
|
+
|
24
|
+
def test_basics(self):
|
25
|
+
tc = timing.TimeIt('node')
|
26
|
+
self.assertFalse(tc.has_started)
|
27
|
+
self.assertEqual(tc.elapse, 0)
|
28
|
+
|
29
|
+
tc.start()
|
30
|
+
self.assertTrue(tc.has_started)
|
31
|
+
self.assertGreater(tc.elapse, 0)
|
32
|
+
|
33
|
+
self.assertTrue(tc.end())
|
34
|
+
self.assertTrue(tc.has_ended)
|
35
|
+
elapse1 = tc.elapse
|
36
|
+
self.assertFalse(tc.end())
|
37
|
+
self.assertEqual(tc.elapse, elapse1)
|
38
|
+
|
39
|
+
def test_timeit(self):
|
40
|
+
with timing.timeit('node') as t:
|
41
|
+
self.assertEqual(t.name, 'node')
|
42
|
+
self.assertIsNotNone(t.start_time)
|
43
|
+
self.assertTrue(t.has_started)
|
44
|
+
self.assertIsNone(t.end_time)
|
45
|
+
self.assertFalse(t.has_ended)
|
46
|
+
time.sleep(0.5)
|
47
|
+
elapse1 = t.elapse
|
48
|
+
self.assertEqual(t.children, [])
|
49
|
+
with timing.timeit('child') as t1:
|
50
|
+
time.sleep(0.5)
|
51
|
+
with timing.timeit('grandchild') as t2:
|
52
|
+
time.sleep(0.5)
|
53
|
+
|
54
|
+
self.assertEqual(t.children, [t1])
|
55
|
+
self.assertEqual(t1.children, [t2])
|
56
|
+
|
57
|
+
r = t.status()
|
58
|
+
self.assertTrue(r['node'].has_started)
|
59
|
+
self.assertGreater(r['node'].elapse, 0)
|
60
|
+
self.assertFalse(r['node'].has_ended)
|
61
|
+
self.assertFalse(r['node'].has_error)
|
62
|
+
self.assertFalse(r['node.child'].has_ended)
|
63
|
+
self.assertTrue(r['node.child.grandchild'].has_ended)
|
64
|
+
|
65
|
+
with self.assertRaisesRegex(ValueError, '.* already exists'):
|
66
|
+
with timing.timeit('grandchild'):
|
67
|
+
pass
|
68
|
+
|
69
|
+
elapse2 = t.elapse
|
70
|
+
self.assertTrue(t.has_ended)
|
71
|
+
self.assertGreater(elapse2, elapse1)
|
72
|
+
time.sleep(0.5)
|
73
|
+
self.assertEqual(elapse2, t.elapse)
|
74
|
+
|
75
|
+
statuss = t.status()
|
76
|
+
self.assertEqual(
|
77
|
+
list(statuss.keys()),
|
78
|
+
['node', 'node.child', 'node.child.grandchild']
|
79
|
+
)
|
80
|
+
self.assertEqual(
|
81
|
+
sorted([v.elapse for v in statuss.values()], reverse=True),
|
82
|
+
[v.elapse for v in statuss.values()],
|
83
|
+
)
|
84
|
+
self.assertTrue(all(v.has_ended for v in statuss.values()))
|
85
|
+
self.assertFalse(any(v.has_error for v in statuss.values()))
|
86
|
+
status = t.status()
|
87
|
+
json_dict = json_conversion.to_json(status)
|
88
|
+
status2 = json_conversion.from_json(json_dict)
|
89
|
+
self.assertIsNot(status2, status)
|
90
|
+
self.assertEqual(status2, status)
|
91
|
+
|
92
|
+
def test_timeit_with_error(self):
|
93
|
+
with self.assertRaises(ValueError):
|
94
|
+
with timing.timeit('node') as t:
|
95
|
+
with timing.timeit('child') as t1:
|
96
|
+
with timing.timeit('grandchild') as t2:
|
97
|
+
raise ValueError('error')
|
98
|
+
|
99
|
+
r = t.status()
|
100
|
+
self.assertTrue(r['node'].has_error)
|
101
|
+
self.assertTrue(t.has_error)
|
102
|
+
self.assertTrue(t.error.tag.startswith('ValueError'))
|
103
|
+
self.assertTrue(r['node'].error.tag.startswith('ValueError'))
|
104
|
+
self.assertTrue(r['node.child'].has_error)
|
105
|
+
self.assertTrue(t1.has_error)
|
106
|
+
self.assertTrue(r['node.child.grandchild'].has_error)
|
107
|
+
self.assertTrue(t2.has_error)
|
108
|
+
|
109
|
+
def test_timeit_summary(self):
|
110
|
+
summary = timing.TimeIt.StatusSummary()
|
111
|
+
self.assertFalse(summary)
|
112
|
+
for i in range(10):
|
113
|
+
with timing.timeit() as t:
|
114
|
+
time.sleep(0.1)
|
115
|
+
with timing.timeit('child'):
|
116
|
+
time.sleep(0.1)
|
117
|
+
try:
|
118
|
+
with timing.timeit('grandchild'):
|
119
|
+
time.sleep(0.1)
|
120
|
+
if i < 2:
|
121
|
+
raise ValueError('error')
|
122
|
+
except ValueError:
|
123
|
+
pass
|
124
|
+
summary.aggregate(t.status())
|
125
|
+
self.assertTrue(summary)
|
126
|
+
self.assertEqual(
|
127
|
+
list(summary.breakdown.keys()),
|
128
|
+
['', 'child', 'child.grandchild']
|
129
|
+
)
|
130
|
+
self.assertEqual(
|
131
|
+
[x.num_started for x in summary.breakdown.values()],
|
132
|
+
[10, 10, 10]
|
133
|
+
)
|
134
|
+
self.assertEqual(
|
135
|
+
[x.num_ended for x in summary.breakdown.values()],
|
136
|
+
[10, 10, 10]
|
137
|
+
)
|
138
|
+
self.assertEqual(
|
139
|
+
[x.num_failed for x in summary.breakdown.values()],
|
140
|
+
[0, 0, 2]
|
141
|
+
)
|
142
|
+
self.assertEqual(
|
143
|
+
summary.breakdown['child.grandchild'].error_tags,
|
144
|
+
{'ValueError': 2},
|
145
|
+
)
|
146
|
+
# Test serialization.
|
147
|
+
json_dict = summary.to_json()
|
148
|
+
summary2 = timing.TimeIt.StatusSummary.from_json(json_dict)
|
149
|
+
self.assertIsNot(summary2, summary)
|
150
|
+
self.assertEqual(summary2.breakdown, summary.breakdown)
|
151
|
+
|
152
|
+
|
153
|
+
if __name__ == '__main__':
|
154
|
+
unittest.main()
|