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.
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.dev202501132210.dist-info}/METADATA +18 -4
  131. pyglove-0.4.5.dev202501132210.dist-info/RECORD +214 -0
  132. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.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.dev202501132210.dist-info}/LICENSE +0 -0
  145. {pyglove-0.4.5.dev20240319.dist-info → pyglove-0.4.5.dev202501132210.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,164 @@
1
+ # Copyright 2022 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
+ # pylint: disable=line-too-long
15
+ """Utility library that provides common traits for objects in Python.
16
+
17
+ Overview
18
+ --------
19
+
20
+ ``pg.utils`` sits at the bottom of all PyGlove modules and empowers other
21
+ modules with the following features:
22
+
23
+ +---------------------+--------------------------------------------+
24
+ | Functionality | API |
25
+ +=====================+============================================+
26
+ | Formatting | :class:`pg.Formattable`, |
27
+ | | |
28
+ | | :func:`pg.format`, |
29
+ | | |
30
+ | | :func:`pg.print`, |
31
+ | | |
32
+ | | :func:`pg.utils.kvlist_str`, |
33
+ | | |
34
+ | | :func:`pg.utils.quote_if_str`, |
35
+ | | |
36
+ | | :func:`pg.utils.message_on_path` |
37
+ +---------------------+--------------------------------------------+
38
+ | Serialization | :class:`pg.JSONConvertible`, |
39
+ | | |
40
+ | | :func:`pg.registered_types`, |
41
+ | | |
42
+ | | :func:`pg.utils.to_json`, |
43
+ | | |
44
+ | | :func:`pg.utils.from_json`, |
45
+ +---------------------+--------------------------------------------+
46
+ | Partial construction| :class:`pg.MaybePartial`, |
47
+ | | |
48
+ | | :const:`pg.MISSING_VALUE`. |
49
+ +---------------------+--------------------------------------------+
50
+ | Hierarchical key | :class:`pg.KeyPath` |
51
+ | representation | |
52
+ +---------------------+--------------------------------------------+
53
+ | Hierarchical object | :func:`pg.utils.traverse` |
54
+ | traversal | |
55
+ +---------------------+--------------------------------------------+
56
+ | Hierarchical object | :func:`pg.utils.transform`, |
57
+ | transformation | |
58
+ | | :func:`pg.utils.merge`, |
59
+ | | |
60
+ | | :func:`pg.utils.canonicalize`, |
61
+ | | |
62
+ | | :func:`pg.utils.flatten` |
63
+ +---------------------+--------------------------------------------+
64
+ | Docstr handling | :class:`pg.docstr`, |
65
+ +---------------------+--------------------------------------------+
66
+ | Error handling | :class:`pg.catch_errors`, |
67
+ +---------------------+--------------------------------------------+
68
+ """
69
+ # pylint: enable=line-too-long
70
+ # pylint: disable=g-bad-import-order
71
+ # pylint: disable=g-importing-member
72
+
73
+ # Handling JSON conversion.
74
+ from pyglove.core.utils.json_conversion import Nestable
75
+ from pyglove.core.utils.json_conversion import JSONValueType
76
+
77
+ from pyglove.core.utils.json_conversion import JSONConvertible
78
+ from pyglove.core.utils.json_conversion import from_json
79
+ from pyglove.core.utils.json_conversion import to_json
80
+ from pyglove.core.utils.json_conversion import registered_types
81
+
82
+ # Handling formatting.
83
+ from pyglove.core.utils.formatting import Formattable
84
+ from pyglove.core.utils.formatting import format # pylint: disable=redefined-builtin
85
+ from pyglove.core.utils.formatting import printv as print # pylint: disable=redefined-builtin
86
+ from pyglove.core.utils.formatting import kvlist_str
87
+ from pyglove.core.utils.formatting import quote_if_str
88
+ from pyglove.core.utils.formatting import maybe_markdown_quote
89
+ from pyglove.core.utils.formatting import comma_delimited_str
90
+ from pyglove.core.utils.formatting import camel_to_snake
91
+ from pyglove.core.utils.formatting import auto_plural
92
+ from pyglove.core.utils.formatting import BracketType
93
+ from pyglove.core.utils.formatting import bracket_chars
94
+ from pyglove.core.utils.formatting import RawText
95
+
96
+ # Context managers for defining the default format for __str__ and __repr__.
97
+ from pyglove.core.utils.formatting import str_format
98
+ from pyglove.core.utils.formatting import repr_format
99
+
100
+ # Value location.
101
+ from pyglove.core.utils.value_location import KeyPath
102
+ from pyglove.core.utils.value_location import KeyPathSet
103
+ from pyglove.core.utils.value_location import StrKey
104
+ from pyglove.core.utils.value_location import message_on_path
105
+
106
+ # Value markers.
107
+ from pyglove.core.utils.missing import MissingValue
108
+ from pyglove.core.utils.missing import MISSING_VALUE
109
+
110
+ # Handling hierarchical.
111
+ from pyglove.core.utils.hierarchical import traverse
112
+ from pyglove.core.utils.hierarchical import transform
113
+ from pyglove.core.utils.hierarchical import flatten
114
+ from pyglove.core.utils.hierarchical import canonicalize
115
+ from pyglove.core.utils.hierarchical import merge
116
+ from pyglove.core.utils.hierarchical import merge_tree
117
+ from pyglove.core.utils.hierarchical import is_partial
118
+ from pyglove.core.utils.hierarchical import try_listify_dict_with_int_keys
119
+
120
+ # Common traits.
121
+ from pyglove.core.utils.common_traits import MaybePartial
122
+ from pyglove.core.utils.common_traits import Functor
123
+
124
+ from pyglove.core.utils.common_traits import explicit_method_override
125
+ from pyglove.core.utils.common_traits import ensure_explicit_method_override
126
+
127
+ # Handling thread local values.
128
+ from pyglove.core.utils.thread_local import thread_local_value_scope
129
+ from pyglove.core.utils.thread_local import thread_local_has
130
+ from pyglove.core.utils.thread_local import thread_local_set
131
+ from pyglove.core.utils.thread_local import thread_local_get
132
+ from pyglove.core.utils.thread_local import thread_local_del
133
+ from pyglove.core.utils.thread_local import thread_local_increment
134
+ from pyglove.core.utils.thread_local import thread_local_decrement
135
+ from pyglove.core.utils.thread_local import thread_local_push
136
+ from pyglove.core.utils.thread_local import thread_local_pop
137
+ from pyglove.core.utils.thread_local import thread_local_peek
138
+
139
+ # Handling docstrings.
140
+ from pyglove.core.utils.docstr_utils import DocStr
141
+ from pyglove.core.utils.docstr_utils import DocStrStyle
142
+ from pyglove.core.utils.docstr_utils import DocStrEntry
143
+ from pyglove.core.utils.docstr_utils import DocStrExample
144
+ from pyglove.core.utils.docstr_utils import DocStrArgument
145
+ from pyglove.core.utils.docstr_utils import DocStrReturns
146
+ from pyglove.core.utils.docstr_utils import DocStrRaises
147
+ from pyglove.core.utils.docstr_utils import docstr
148
+
149
+ # Handling exceptions.
150
+ from pyglove.core.utils.error_utils import catch_errors
151
+ from pyglove.core.utils.error_utils import CatchErrorsContext
152
+ from pyglove.core.utils.error_utils import ErrorInfo
153
+
154
+ # Timing.
155
+ from pyglove.core.utils.timing import timeit
156
+ from pyglove.core.utils.timing import TimeIt
157
+
158
+ # Text color.
159
+ from pyglove.core.utils.text_color import colored
160
+ from pyglove.core.utils.text_color import colored_block
161
+ from pyglove.core.utils.text_color import decolor
162
+
163
+ # pylint: enable=g-importing-member
164
+ # pylint: enable=g-bad-import-order
@@ -14,75 +14,11 @@
14
14
  """Common traits for Python objects.
15
15
 
16
16
  This file defines interfaces for describing the common traits of a Python
17
- object, for example, partiality (MaybePartial), formatting (Formattable),
18
- functor (Functor).
17
+ object, for example, partiality (MaybePartial), functor (Functor).
19
18
  """
20
19
 
21
20
  import abc
22
- from typing import Any, ContextManager, Dict, Optional
23
- from pyglove.core.object_utils import thread_local
24
-
25
-
26
- _TLS_STR_FORMAT_KWARGS = '_str_format_kwargs'
27
- _TLS_REPR_FORMAT_KWARGS = '_repr_format_kwargs'
28
-
29
-
30
- def str_format(**kwargs) -> ContextManager[Dict[str, Any]]:
31
- """Context manager for setting the default format kwargs for __str__."""
32
- return thread_local.thread_local_arg_scope(_TLS_STR_FORMAT_KWARGS, **kwargs)
33
-
34
-
35
- def repr_format(**kwargs) -> ContextManager[Dict[str, Any]]:
36
- """Context manager for setting the default format kwargs for __repr__."""
37
- return thread_local.thread_local_arg_scope(_TLS_REPR_FORMAT_KWARGS, **kwargs)
38
-
39
-
40
- class Formattable(metaclass=abc.ABCMeta):
41
- """Interface for classes whose instances can be pretty-formatted.
42
-
43
- This interface overrides the default ``__repr__`` and ``__str__`` method, thus
44
- all ``Formattable`` objects can be printed nicely.
45
-
46
- All symbolic types implement this interface.
47
- """
48
-
49
- # Additional format keyword arguments for `__str__`.
50
- __str_format_kwargs__ = dict(compact=False, verbose=True)
51
-
52
- # Additional format keyword arguments for `__repr__`.
53
- __repr_format_kwargs__ = dict(compact=True)
54
-
55
- @abc.abstractmethod
56
- def format(self,
57
- compact: bool = False,
58
- verbose: bool = True,
59
- root_indent: int = 0,
60
- **kwargs) -> str:
61
- """Formats this object into a string representation.
62
-
63
- Args:
64
- compact: If True, this object will be formatted into a single line.
65
- verbose: If True, this object will be formatted with verbosity.
66
- Subclasses should define `verbosity` on their own.
67
- root_indent: The start indent level for this object if the output is a
68
- multi-line string.
69
- **kwargs: Subclass specific keyword arguments.
70
-
71
- Returns:
72
- A string of formatted object.
73
- """
74
-
75
- def __str__(self) -> str:
76
- """Returns the full (maybe multi-line) representation of this object."""
77
- kwargs = dict(self.__str_format_kwargs__)
78
- kwargs.update(thread_local.thread_local_kwargs(_TLS_STR_FORMAT_KWARGS))
79
- return self.format(**kwargs)
80
-
81
- def __repr__(self) -> str:
82
- """Returns a single-line representation of this object."""
83
- kwargs = dict(self.__repr_format_kwargs__)
84
- kwargs.update(thread_local.thread_local_kwargs(_TLS_REPR_FORMAT_KWARGS))
85
- return self.format(**kwargs)
21
+ from typing import Any, Dict, Optional, Union
86
22
 
87
23
 
88
24
  class MaybePartial(metaclass=abc.ABCMeta):
@@ -111,7 +47,7 @@ class MaybePartial(metaclass=abc.ABCMeta):
111
47
  return len(self.missing_values()) > 0 # pylint: disable=g-explicit-length-test
112
48
 
113
49
  @abc.abstractmethod
114
- def missing_values(self, flatten: bool = True) -> Dict[str, Any]: # pylint: disable=redefined-outer-name
50
+ def missing_values(self, flatten: bool = True) -> Dict[Union[str, int], Any]: # pylint: disable=redefined-outer-name
115
51
  """Returns missing values from this object.
116
52
 
117
53
  Args:
@@ -0,0 +1,36 @@
1
+ # Copyright 2022 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 unittest
15
+ from pyglove.core.utils import common_traits
16
+
17
+
18
+ class ExplicitlyOverrideTest(unittest.TestCase):
19
+
20
+ def test_explicitly_override(self):
21
+ class A:
22
+
23
+ @common_traits.explicit_method_override
24
+ def __init__(self, x, y):
25
+ pass
26
+
27
+ def bar(self):
28
+ pass
29
+
30
+ common_traits.ensure_explicit_method_override(A.__init__)
31
+ with self.assertRaisesRegex(TypeError, '.* is a PyGlove managed method'):
32
+ common_traits.ensure_explicit_method_override(A.bar)
33
+
34
+
35
+ if __name__ == '__main__':
36
+ unittest.main()
@@ -15,6 +15,7 @@
15
15
 
16
16
  import dataclasses
17
17
  import enum
18
+ import inspect
18
19
  from typing import Any, Dict, List, Optional
19
20
 
20
21
  import docstring_parser
@@ -74,6 +75,28 @@ class DocStr:
74
75
  raises: List[DocStrRaises]
75
76
  blank_after_short_description: bool = True
76
77
 
78
+ @property
79
+ def description(self) -> Optional[str]:
80
+ """Returns short_description + long_description."""
81
+ if self.short_description is None and self.long_description is None:
82
+ return None
83
+ description = self.short_description or ''
84
+ if self.blank_after_short_description:
85
+ description += '\n'
86
+ if self.long_description:
87
+ description += '\n' + self.long_description
88
+ return description.rstrip('\n')
89
+
90
+ def parameter(self, param: inspect.Parameter) -> Optional[DocStrArgument]:
91
+ """Returns doc str for an inspected parameter."""
92
+ if param.kind == inspect.Parameter.VAR_POSITIONAL:
93
+ name = f'*{param.name}'
94
+ elif param.kind == inspect.Parameter.VAR_KEYWORD:
95
+ name = f'**{param.name}'
96
+ else:
97
+ name = param.name
98
+ return self.args.get(name)
99
+
77
100
  @classmethod
78
101
  def parse(cls, text: str, style: Optional[DocStrStyle] = None) -> 'DocStr':
79
102
  """Parses a docstring."""
@@ -11,10 +11,9 @@
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.docstr_utils."""
15
-
14
+ import inspect
16
15
  import unittest
17
- from pyglove.core.object_utils import docstr_utils
16
+ from pyglove.core.utils import docstr_utils
18
17
 
19
18
 
20
19
  class DocStrTest(unittest.TestCase):
@@ -57,7 +56,16 @@ class DocStrTest(unittest.TestCase):
57
56
  del args, kwargs
58
57
  return x + y
59
58
 
60
- self.assertEqual(docstr_utils.docstr(my_sum), docstr_utils.DocStr(
59
+ docstr = docstr_utils.docstr(my_sum)
60
+ self.assertEqual(
61
+ docstr.description,
62
+ inspect.cleandoc('''
63
+ Returns the sum of two integers.
64
+
65
+ This function will return the sum of two integers.
66
+ ''')
67
+ )
68
+ self.assertEqual(docstr, docstr_utils.DocStr(
61
69
  style=docstr_utils.DocStrStyle.GOOGLE,
62
70
  short_description='Returns the sum of two integers.',
63
71
  long_description='This function will return the sum of two integers.',
@@ -93,6 +101,11 @@ class DocStrTest(unittest.TestCase):
93
101
  )
94
102
  ]
95
103
  ))
104
+ sig = inspect.signature(my_sum)
105
+ self.assertIsNotNone(docstr.parameter(sig.parameters['x']))
106
+ self.assertIsNotNone(docstr.parameter(sig.parameters['y']))
107
+ self.assertIsNotNone(docstr.parameter(sig.parameters['args']))
108
+ self.assertIsNotNone(docstr.parameter(sig.parameters['kwargs']))
96
109
 
97
110
  class Foo:
98
111
  pass
@@ -100,6 +113,25 @@ class DocStrTest(unittest.TestCase):
100
113
  self.assertIsNone(docstr_utils.docstr(Foo))
101
114
  self.assertIsNone(docstr_utils.docstr(None))
102
115
 
116
+ class Bar:
117
+ """bar."""
118
+
119
+ self.assertEqual(docstr_utils.docstr(Bar).description, 'bar.')
120
+
121
+ # pylint: disable=g-classes-have-attributes
122
+ # pylint: disable=g-short-docstring-punctuation
123
+ # pylint: disable=g-space-before-docstring-summary
124
+ # pylint: disable=g-no-space-after-docstring-summary
125
+ class Baz:
126
+ """
127
+ Args:
128
+ x: int
129
+ """
130
+ # pylint: enable=g-space-before-docstring-summary
131
+ # pylint: enable=g-short-docstring-punctuation
132
+ # pylint: enable=g-classes-have-attributes
133
+ self.assertIsNone(docstr_utils.docstr(Baz).description)
134
+
103
135
 
104
136
  if __name__ == '__main__':
105
137
  unittest.main()
@@ -17,28 +17,92 @@ import contextlib
17
17
  import dataclasses
18
18
  import inspect
19
19
  import re
20
- from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
20
+ import sys
21
+ import traceback
22
+ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
23
+
24
+ from pyglove.core.utils import formatting
25
+ from pyglove.core.utils import json_conversion
26
+
27
+
28
+ @dataclasses.dataclass(frozen=True)
29
+ class ErrorInfo(json_conversion.JSONConvertible, formatting.Formattable):
30
+ """Serializable error information.
31
+
32
+ Attributes:
33
+ tag: A path of the error types in the exception chain. For example,
34
+ `ValueError.ZeroDivisionError` means the error is a `ZeroDivisionError`
35
+ raised at the first place and then reraised as a `ValueError`.
36
+ description: The description of the error.
37
+ stacktrace: The stacktrace of the error.
38
+ """
39
+
40
+ tag: str
41
+ description: str
42
+ stacktrace: str
43
+
44
+ @classmethod
45
+ def _compute_tag(cls, error: BaseException):
46
+ error_types = []
47
+ while error is not None:
48
+ error_types.append(error.__class__.__name__)
49
+ error = getattr(error, 'cause', error.__cause__)
50
+ return '.'.join(error_types)
51
+
52
+ @classmethod
53
+ def from_exception(cls, error: BaseException) -> 'ErrorInfo':
54
+ """Creates an error info from an exception."""
55
+ return cls(
56
+ tag=cls._compute_tag(error),
57
+ description=str(error),
58
+ stacktrace=''.join(
59
+ traceback.format_exception(*sys.exc_info())
60
+ )
61
+ )
62
+
63
+ def to_json(self, **kwargs) -> Dict[str, Any]:
64
+ return self.to_json_dict(
65
+ fields=dict(
66
+ tag=(self.tag, None),
67
+ description=(self.description, None),
68
+ stacktrace=(self.stacktrace, None),
69
+ ),
70
+ exclude_default=True,
71
+ **kwargs,
72
+ )
73
+
74
+ def format(self, *args, **kwargs) -> str:
75
+ return formatting.kvlist_str(
76
+ [
77
+ ('tag', self.tag, None),
78
+ ('description', self.description, None),
79
+ ('stacktrace', self.stacktrace, None),
80
+ ],
81
+ *args,
82
+ label=self.__class__.__name__,
83
+ **kwargs,
84
+ )
21
85
 
22
86
 
23
87
  @dataclasses.dataclass()
24
88
  class CatchErrorsContext:
25
89
  """Context for pg.catch_errors."""
26
- error: Optional[Exception] = None
90
+ error: Optional[BaseException] = None
27
91
 
28
92
 
29
93
  @contextlib.contextmanager
30
94
  def catch_errors(
31
95
  errors: Union[
32
- Union[Type[Exception], Tuple[Exception, str]],
33
- Sequence[Union[Type[Exception], Tuple[Exception, str]]],
96
+ Union[Type[BaseException], Tuple[Type[BaseException], str]],
97
+ Sequence[Union[Type[BaseException], Tuple[Type[BaseException], str]]],
34
98
  ],
35
- error_handler: Optional[Callable[[Exception], None]] = None
99
+ error_handler: Optional[Callable[[BaseException], None]] = None
36
100
  ):
37
101
  """Context manager for catching user-specified exceptions.
38
102
 
39
103
  Examples::
40
104
 
41
- with pg.object_utils.catch_errors(
105
+ with pg.utils.catch_errors(
42
106
  [
43
107
  RuntimeErrror,
44
108
  (ValueError, 'Input is wrong.')
@@ -71,7 +135,7 @@ def catch_errors(
71
135
  ):
72
136
  errors = [errors]
73
137
 
74
- error_mapping: Dict[Type[Exception], List[str]] = {}
138
+ error_mapping: Dict[Type[BaseException], List[str]] = {}
75
139
  for error_type in errors:
76
140
  regex = None
77
141
  if isinstance(error_type, tuple):
@@ -82,7 +146,9 @@ def catch_errors(
82
146
  f'to match. Encountered: {error_type!r}.'
83
147
  )
84
148
  error_type, regex = error_type
85
- if not (inspect.isclass(error_type) and issubclass(error_type, Exception)):
149
+ if not (
150
+ inspect.isclass(error_type) and issubclass(error_type, BaseException)
151
+ ):
86
152
  raise TypeError(f'Exception contains non-except types: {error_type!r}.')
87
153
  if error_type not in error_mapping:
88
154
  error_mapping[error_type] = []
@@ -93,7 +159,7 @@ def catch_errors(
93
159
  try:
94
160
  yield context
95
161
  except tuple(error_mapping.keys()) as e:
96
- error_message = str(e)
162
+ error_message = e.__class__.__name__ + ': ' + str(e)
97
163
  found_match = False
98
164
  for error_type, error_regexes in error_mapping.items():
99
165
  if isinstance(e, error_type):
@@ -101,6 +167,9 @@ def catch_errors(
101
167
  found_match = True
102
168
  else:
103
169
  for regex in error_regexes:
170
+ assert regex is not None
171
+ if not regex.startswith(('^', '.*')):
172
+ regex = '.*' + regex
104
173
  if re.match(regex, error_message):
105
174
  found_match = True
106
175
  break
@@ -11,10 +11,64 @@
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.error_utils."""
15
-
14
+ import inspect
16
15
  import unittest
17
- from pyglove.core.object_utils import error_utils
16
+ from pyglove.core.utils import error_utils
17
+
18
+
19
+ class ErrorInfoTest(unittest.TestCase):
20
+ """Tests for ErrorInfo."""
21
+
22
+ def test_from_exception(self):
23
+
24
+ def foo():
25
+ return 1 / 0
26
+
27
+ def bar():
28
+ try:
29
+ foo()
30
+ except ZeroDivisionError as e:
31
+ raise ValueError('Bad call to `foo`') from e
32
+
33
+ error_info = None
34
+ try:
35
+ bar()
36
+ except ValueError as e:
37
+ error_info = error_utils.ErrorInfo.from_exception(e)
38
+ self.assertIsNotNone(error_info)
39
+ self.assertEqual(error_info.tag, 'ValueError.ZeroDivisionError')
40
+ self.assertEqual(error_info.description, 'Bad call to `foo`')
41
+ self.assertIn('Traceback (most recent call last)', error_info.stacktrace)
42
+
43
+ def test_to_json(self):
44
+ error_info = error_utils.ErrorInfo(
45
+ tag='ValueError.ZeroDivisionError',
46
+ description='Bad call to `foo`',
47
+ stacktrace='Traceback (most recent call last)',
48
+ )
49
+ json_dict = error_info.to_json()
50
+ error_info2 = error_utils.ErrorInfo.from_json(json_dict)
51
+ self.assertIsNot(error_info2, error_info)
52
+ self.assertEqual(error_info2, error_info)
53
+
54
+ def test_format(self):
55
+ error_info = error_utils.ErrorInfo(
56
+ tag='ValueError.ZeroDivisionError',
57
+ description='Bad call to `foo`',
58
+ stacktrace='Traceback (most recent call last)',
59
+ )
60
+ self.assertEqual(
61
+ error_info.format(compact=False),
62
+ inspect.cleandoc(
63
+ """
64
+ ErrorInfo(
65
+ tag='ValueError.ZeroDivisionError',
66
+ description='Bad call to `foo`',
67
+ stacktrace='Traceback (most recent call last)'
68
+ )
69
+ """
70
+ )
71
+ )
18
72
 
19
73
 
20
74
  class CatchErrorsTest(unittest.TestCase):
@@ -36,11 +90,13 @@ class CatchErrorsTest(unittest.TestCase):
36
90
 
37
91
  self.assert_caught_error(foo, ValueError)
38
92
  self.assert_caught_error(foo, (ValueError,))
93
+ self.assert_caught_error(foo, (Exception, 'ValueError'))
39
94
  self.assert_caught_error(foo, (KeyError, ValueError))
40
- self.assert_caught_error(foo, (ValueError, '.* an error'))
41
- self.assert_caught_error(foo, (KeyError, (ValueError, '.* an error'),))
95
+ self.assert_caught_error(foo, (ValueError, 'an error'))
96
+ self.assert_caught_error(foo, (KeyError, (ValueError, 'an error'),))
42
97
 
43
98
  self.assert_propagate_error(foo, KeyError)
99
+ self.assert_propagate_error(foo, (ValueError, '^an error'))
44
100
  self.assert_propagate_error(foo, (ValueError, 'something else'))
45
101
 
46
102
  def test_catch_errors_with_error_handler(self):