pyglove 0.4.5.dev20240319__py3-none-any.whl → 0.4.5.dev202501140808__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.
Files changed (145) hide show
  1. pyglove/core/__init__.py +54 -20
  2. pyglove/core/coding/__init__.py +42 -0
  3. pyglove/core/coding/errors.py +111 -0
  4. pyglove/core/coding/errors_test.py +98 -0
  5. pyglove/core/coding/execution.py +309 -0
  6. pyglove/core/coding/execution_test.py +333 -0
  7. pyglove/core/{object_utils/codegen.py → coding/function_generation.py} +10 -4
  8. pyglove/core/{object_utils/codegen_test.py → coding/function_generation_test.py} +5 -7
  9. pyglove/core/coding/parsing.py +153 -0
  10. pyglove/core/coding/parsing_test.py +150 -0
  11. pyglove/core/coding/permissions.py +100 -0
  12. pyglove/core/coding/permissions_test.py +93 -0
  13. pyglove/core/geno/base.py +54 -41
  14. pyglove/core/geno/base_test.py +2 -4
  15. pyglove/core/geno/categorical.py +37 -28
  16. pyglove/core/geno/custom.py +19 -16
  17. pyglove/core/geno/numerical.py +20 -17
  18. pyglove/core/geno/space.py +4 -5
  19. pyglove/core/hyper/base.py +6 -6
  20. pyglove/core/hyper/categorical.py +94 -55
  21. pyglove/core/hyper/custom.py +7 -7
  22. pyglove/core/hyper/custom_test.py +9 -10
  23. pyglove/core/hyper/derived.py +30 -22
  24. pyglove/core/hyper/derived_test.py +2 -4
  25. pyglove/core/hyper/dynamic_evaluation.py +5 -6
  26. pyglove/core/hyper/evolvable.py +57 -46
  27. pyglove/core/hyper/numerical.py +48 -24
  28. pyglove/core/hyper/numerical_test.py +9 -9
  29. pyglove/core/hyper/object_template.py +58 -46
  30. pyglove/core/io/__init__.py +1 -0
  31. pyglove/core/io/file_system.py +17 -7
  32. pyglove/core/io/file_system_test.py +2 -0
  33. pyglove/core/io/sequence.py +299 -0
  34. pyglove/core/io/sequence_test.py +124 -0
  35. pyglove/core/logging_test.py +0 -2
  36. pyglove/core/patching/object_factory.py +4 -4
  37. pyglove/core/patching/pattern_based.py +4 -4
  38. pyglove/core/patching/rule_based.py +17 -5
  39. pyglove/core/patching/rule_based_test.py +27 -4
  40. pyglove/core/symbolic/__init__.py +2 -7
  41. pyglove/core/symbolic/base.py +320 -183
  42. pyglove/core/symbolic/base_test.py +123 -19
  43. pyglove/core/symbolic/boilerplate.py +7 -13
  44. pyglove/core/symbolic/boilerplate_test.py +25 -23
  45. pyglove/core/symbolic/class_wrapper.py +48 -45
  46. pyglove/core/symbolic/class_wrapper_test.py +2 -2
  47. pyglove/core/symbolic/compounding.py +9 -15
  48. pyglove/core/symbolic/compounding_test.py +2 -4
  49. pyglove/core/symbolic/dict.py +154 -110
  50. pyglove/core/symbolic/dict_test.py +238 -130
  51. pyglove/core/symbolic/diff.py +199 -10
  52. pyglove/core/symbolic/diff_test.py +226 -0
  53. pyglove/core/symbolic/flags.py +1 -1
  54. pyglove/core/symbolic/functor.py +29 -26
  55. pyglove/core/symbolic/functor_test.py +102 -50
  56. pyglove/core/symbolic/inferred.py +2 -2
  57. pyglove/core/symbolic/list.py +81 -50
  58. pyglove/core/symbolic/list_test.py +119 -97
  59. pyglove/core/symbolic/object.py +225 -113
  60. pyglove/core/symbolic/object_test.py +320 -108
  61. pyglove/core/symbolic/origin.py +17 -14
  62. pyglove/core/symbolic/origin_test.py +4 -2
  63. pyglove/core/symbolic/pure_symbolic.py +4 -3
  64. pyglove/core/symbolic/ref.py +108 -21
  65. pyglove/core/symbolic/ref_test.py +93 -0
  66. pyglove/core/symbolic/symbolize_test.py +10 -2
  67. pyglove/core/tuning/local_backend.py +2 -2
  68. pyglove/core/tuning/protocols.py +3 -3
  69. pyglove/core/tuning/sample_test.py +3 -3
  70. pyglove/core/typing/__init__.py +14 -5
  71. pyglove/core/typing/annotation_conversion.py +43 -27
  72. pyglove/core/typing/annotation_conversion_test.py +23 -0
  73. pyglove/core/typing/callable_ext.py +241 -3
  74. pyglove/core/typing/callable_ext_test.py +255 -0
  75. pyglove/core/typing/callable_signature.py +510 -66
  76. pyglove/core/typing/callable_signature_test.py +619 -99
  77. pyglove/core/typing/class_schema.py +229 -154
  78. pyglove/core/typing/class_schema_test.py +149 -95
  79. pyglove/core/typing/custom_typing.py +5 -4
  80. pyglove/core/typing/inspect.py +63 -0
  81. pyglove/core/typing/inspect_test.py +39 -0
  82. pyglove/core/typing/key_specs.py +10 -11
  83. pyglove/core/typing/key_specs_test.py +7 -4
  84. pyglove/core/typing/type_conversion.py +4 -5
  85. pyglove/core/typing/type_conversion_test.py +12 -12
  86. pyglove/core/typing/typed_missing.py +6 -7
  87. pyglove/core/typing/typed_missing_test.py +7 -8
  88. pyglove/core/typing/value_specs.py +604 -362
  89. pyglove/core/typing/value_specs_test.py +328 -90
  90. pyglove/core/utils/__init__.py +164 -0
  91. pyglove/core/{object_utils → utils}/common_traits.py +3 -67
  92. pyglove/core/utils/common_traits_test.py +36 -0
  93. pyglove/core/{object_utils → utils}/docstr_utils.py +23 -0
  94. pyglove/core/{object_utils → utils}/docstr_utils_test.py +36 -4
  95. pyglove/core/{object_utils → utils}/error_utils.py +78 -9
  96. pyglove/core/{object_utils → utils}/error_utils_test.py +61 -5
  97. pyglove/core/utils/formatting.py +464 -0
  98. pyglove/core/utils/formatting_test.py +453 -0
  99. pyglove/core/{object_utils → utils}/hierarchical.py +23 -25
  100. pyglove/core/{object_utils → utils}/hierarchical_test.py +3 -5
  101. pyglove/core/{object_utils → utils}/json_conversion.py +177 -52
  102. pyglove/core/{object_utils → utils}/json_conversion_test.py +97 -16
  103. pyglove/core/{object_utils → utils}/missing.py +3 -3
  104. pyglove/core/{object_utils → utils}/missing_test.py +2 -4
  105. pyglove/core/utils/text_color.py +128 -0
  106. pyglove/core/utils/text_color_test.py +94 -0
  107. pyglove/core/{object_utils → utils}/thread_local_test.py +1 -3
  108. pyglove/core/utils/timing.py +236 -0
  109. pyglove/core/utils/timing_test.py +154 -0
  110. pyglove/core/{object_utils → utils}/value_location.py +275 -6
  111. pyglove/core/utils/value_location_test.py +707 -0
  112. pyglove/core/views/__init__.py +32 -0
  113. pyglove/core/views/base.py +804 -0
  114. pyglove/core/views/base_test.py +580 -0
  115. pyglove/core/views/html/__init__.py +27 -0
  116. pyglove/core/views/html/base.py +547 -0
  117. pyglove/core/views/html/base_test.py +830 -0
  118. pyglove/core/views/html/controls/__init__.py +35 -0
  119. pyglove/core/views/html/controls/base.py +275 -0
  120. pyglove/core/views/html/controls/label.py +207 -0
  121. pyglove/core/views/html/controls/label_test.py +157 -0
  122. pyglove/core/views/html/controls/progress_bar.py +183 -0
  123. pyglove/core/views/html/controls/progress_bar_test.py +97 -0
  124. pyglove/core/views/html/controls/tab.py +320 -0
  125. pyglove/core/views/html/controls/tab_test.py +87 -0
  126. pyglove/core/views/html/controls/tooltip.py +99 -0
  127. pyglove/core/views/html/controls/tooltip_test.py +99 -0
  128. pyglove/core/views/html/tree_view.py +1517 -0
  129. pyglove/core/views/html/tree_view_test.py +1461 -0
  130. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501140808.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/WHEEL +1 -1
  133. pyglove/core/object_utils/__init__.py +0 -154
  134. pyglove/core/object_utils/common_traits_test.py +0 -82
  135. pyglove/core/object_utils/formatting.py +0 -234
  136. pyglove/core/object_utils/formatting_test.py +0 -223
  137. pyglove/core/object_utils/value_location_test.py +0 -385
  138. pyglove/core/symbolic/schema_utils.py +0 -327
  139. pyglove/core/symbolic/schema_utils_test.py +0 -57
  140. pyglove/core/typing/class_schema_utils.py +0 -202
  141. pyglove/core/typing/class_schema_utils_test.py +0 -194
  142. pyglove-0.4.5.dev20240319.dist-info/RECORD +0 -185
  143. /pyglove/core/{object_utils → utils}/thread_local.py +0 -0
  144. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.dist-info}/LICENSE +0 -0
  145. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501140808.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.object_utils import thread_local
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()