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,464 @@
|
|
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
|
+
"""Utilities for formatting objects."""
|
15
|
+
|
16
|
+
import abc
|
17
|
+
import enum
|
18
|
+
import io
|
19
|
+
import sys
|
20
|
+
from typing import Any, Callable, ContextManager, Dict, List, Optional, Sequence, Set, Tuple
|
21
|
+
from pyglove.core.utils import thread_local
|
22
|
+
|
23
|
+
|
24
|
+
_TLS_STR_FORMAT_KWARGS = '_str_format_kwargs'
|
25
|
+
_TLS_REPR_FORMAT_KWARGS = '_repr_format_kwargs'
|
26
|
+
|
27
|
+
|
28
|
+
CustomFormatFn = Callable[
|
29
|
+
[
|
30
|
+
Any, # Value to format.
|
31
|
+
int, # Root indent.
|
32
|
+
],
|
33
|
+
# Returns a string or None. If None, the default `pg.format` will be used.
|
34
|
+
Optional[str]
|
35
|
+
]
|
36
|
+
|
37
|
+
|
38
|
+
def str_format(**kwargs) -> ContextManager[Dict[str, Any]]:
|
39
|
+
"""Context manager for setting the default format kwargs for __str__."""
|
40
|
+
return thread_local.thread_local_arg_scope(_TLS_STR_FORMAT_KWARGS, **kwargs)
|
41
|
+
|
42
|
+
|
43
|
+
def repr_format(**kwargs) -> ContextManager[Dict[str, Any]]:
|
44
|
+
"""Context manager for setting the default format kwargs for __repr__."""
|
45
|
+
return thread_local.thread_local_arg_scope(_TLS_REPR_FORMAT_KWARGS, **kwargs)
|
46
|
+
|
47
|
+
|
48
|
+
class Formattable(metaclass=abc.ABCMeta):
|
49
|
+
"""Interface for classes whose instances can be pretty-formatted.
|
50
|
+
|
51
|
+
This interface overrides the default ``__repr__`` and ``__str__`` method, thus
|
52
|
+
all ``Formattable`` objects can be printed nicely.
|
53
|
+
|
54
|
+
All symbolic types implement this interface.
|
55
|
+
"""
|
56
|
+
|
57
|
+
# Additional format keyword arguments for `__str__`.
|
58
|
+
__str_format_kwargs__ = dict(compact=False, verbose=True)
|
59
|
+
|
60
|
+
# Additional format keyword arguments for `__repr__`.
|
61
|
+
__repr_format_kwargs__ = dict(compact=True)
|
62
|
+
|
63
|
+
@abc.abstractmethod
|
64
|
+
def format(self,
|
65
|
+
compact: bool = False,
|
66
|
+
verbose: bool = True,
|
67
|
+
root_indent: int = 0,
|
68
|
+
**kwargs) -> str:
|
69
|
+
"""Formats this object into a string representation.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
compact: If True, this object will be formatted into a single line.
|
73
|
+
verbose: If True, this object will be formatted with verbosity.
|
74
|
+
Subclasses should define `verbosity` on their own.
|
75
|
+
root_indent: The start indent level for this object if the output is a
|
76
|
+
multi-line string.
|
77
|
+
**kwargs: Subclass specific keyword arguments.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
A string of formatted object.
|
81
|
+
"""
|
82
|
+
|
83
|
+
def __str__(self) -> str:
|
84
|
+
"""Returns the full (maybe multi-line) representation of this object."""
|
85
|
+
# NOTE(daiyip): we delegate the formatting logic to `pg.format` instead of
|
86
|
+
# `Formattable.format` as `pg.format` could add common functionalities such
|
87
|
+
# as `markdown` support.
|
88
|
+
return format(self, **self.__str_kwargs__())
|
89
|
+
|
90
|
+
def __str_kwargs__(self) -> Dict[str, Any]:
|
91
|
+
"""Returns the default format kwargs for __str__."""
|
92
|
+
kwargs = dict(self.__str_format_kwargs__)
|
93
|
+
kwargs.update(thread_local.thread_local_kwargs(_TLS_STR_FORMAT_KWARGS))
|
94
|
+
return kwargs
|
95
|
+
|
96
|
+
def __repr__(self) -> str:
|
97
|
+
"""Returns a single-line representation of this object."""
|
98
|
+
# NOTE(daiyip): we delegate the formatting logic to `pg.format` instead of
|
99
|
+
# `Formattable.format` as `pg.format` could add common functionalities such
|
100
|
+
# as `markdown` support.
|
101
|
+
return format(self, **self.__repr_kwargs__())
|
102
|
+
|
103
|
+
def __repr_kwargs__(self) -> Dict[str, Any]:
|
104
|
+
"""Returns the default format kwargs for __repr__."""
|
105
|
+
kwargs = dict(self.__repr_format_kwargs__)
|
106
|
+
kwargs.update(thread_local.thread_local_kwargs(_TLS_REPR_FORMAT_KWARGS))
|
107
|
+
return kwargs
|
108
|
+
|
109
|
+
|
110
|
+
class RawText(Formattable):
|
111
|
+
"""Raw text."""
|
112
|
+
|
113
|
+
def __init__(self, text: str):
|
114
|
+
self.text = text
|
115
|
+
|
116
|
+
def format(self, *args, **kwargs):
|
117
|
+
del args, kwargs
|
118
|
+
return self.text
|
119
|
+
|
120
|
+
def __eq__(self, other: Any) -> bool:
|
121
|
+
if isinstance(other, RawText):
|
122
|
+
return self.text == other.text
|
123
|
+
elif isinstance(other, str):
|
124
|
+
return self.text == other
|
125
|
+
return False
|
126
|
+
|
127
|
+
def __ne__(self, other: Any) -> bool:
|
128
|
+
return not self.__eq__(other)
|
129
|
+
|
130
|
+
|
131
|
+
class BracketType(enum.IntEnum):
|
132
|
+
"""Bracket types used for complex type formatting."""
|
133
|
+
# Round bracket.
|
134
|
+
ROUND = 0
|
135
|
+
|
136
|
+
# Square bracket.
|
137
|
+
SQUARE = 1
|
138
|
+
|
139
|
+
# Curly bracket.
|
140
|
+
CURLY = 2
|
141
|
+
|
142
|
+
|
143
|
+
_BRACKET_CHARS = [
|
144
|
+
('(', ')'),
|
145
|
+
('[', ']'),
|
146
|
+
('{', '}'),
|
147
|
+
]
|
148
|
+
|
149
|
+
|
150
|
+
def bracket_chars(bracket_type: BracketType) -> Tuple[str, str]:
|
151
|
+
"""Gets bracket character."""
|
152
|
+
return _BRACKET_CHARS[int(bracket_type)]
|
153
|
+
|
154
|
+
|
155
|
+
def kvlist_str(
|
156
|
+
kvlist: List[Tuple[str, Any, Any]],
|
157
|
+
compact: bool = True,
|
158
|
+
verbose: bool = False,
|
159
|
+
root_indent: int = 0,
|
160
|
+
*,
|
161
|
+
label: Optional[str] = None,
|
162
|
+
bracket_type: BracketType = BracketType.ROUND,
|
163
|
+
custom_format: Optional[CustomFormatFn] = None,
|
164
|
+
**kwargs,
|
165
|
+
) -> str:
|
166
|
+
"""Formats a list key/value pairs into a comma delimited string.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
kvlist: List of tuples in format of
|
170
|
+
(key, value, default_value or a tuple of default values)
|
171
|
+
compact: If True, format value in kvlist in compact form.
|
172
|
+
verbose: If True, format value in kvlist in verbose.
|
173
|
+
root_indent: The indent should be applied for values in kvlist if they are
|
174
|
+
multi-line.
|
175
|
+
label: (Optional) If not None, add label to brace all kv pairs.
|
176
|
+
bracket_type: Bracket type used for embracing the kv pairs. Applicable only
|
177
|
+
when `name` is not None.
|
178
|
+
custom_format: An optional custom format function, which will be applied to
|
179
|
+
each value (and child values) in kvlist. If the function returns None, it
|
180
|
+
will fall back to the default `pg.format`.
|
181
|
+
**kwargs: Keyword arguments that will be passed through unto child
|
182
|
+
``Formattable`` objects.
|
183
|
+
Returns:
|
184
|
+
A formatted string from a list of key/value pairs delimited by comma.
|
185
|
+
"""
|
186
|
+
s = io.StringIO()
|
187
|
+
is_first = True
|
188
|
+
bracket_start, bracket_end = bracket_chars(bracket_type)
|
189
|
+
|
190
|
+
child_indent = (root_indent + 1) if label else root_indent
|
191
|
+
body = io.StringIO()
|
192
|
+
|
193
|
+
for k, v, d in kvlist:
|
194
|
+
if isinstance(d, tuple):
|
195
|
+
include_pair = True
|
196
|
+
for sd in d:
|
197
|
+
if sd == v:
|
198
|
+
include_pair = False
|
199
|
+
break
|
200
|
+
else:
|
201
|
+
include_pair = v != d
|
202
|
+
if include_pair:
|
203
|
+
if not is_first:
|
204
|
+
body.write(',')
|
205
|
+
body.write(' ' if compact else '\n')
|
206
|
+
v = format(
|
207
|
+
v, compact=compact,
|
208
|
+
verbose=verbose,
|
209
|
+
root_indent=child_indent,
|
210
|
+
custom_format=custom_format,
|
211
|
+
**kwargs
|
212
|
+
)
|
213
|
+
if not compact:
|
214
|
+
body.write(_indent('', child_indent))
|
215
|
+
if k:
|
216
|
+
body.write(f'{k}={str_ext(v, custom_format, child_indent)}')
|
217
|
+
else:
|
218
|
+
body.write(str_ext(v, custom_format, child_indent))
|
219
|
+
is_first = False
|
220
|
+
|
221
|
+
if label and not is_first and not compact:
|
222
|
+
body.write('\n')
|
223
|
+
|
224
|
+
body = body.getvalue()
|
225
|
+
if label is None:
|
226
|
+
return body
|
227
|
+
else:
|
228
|
+
s.write(label)
|
229
|
+
s.write(bracket_start)
|
230
|
+
if body:
|
231
|
+
if not compact:
|
232
|
+
s.write('\n')
|
233
|
+
s.write(body)
|
234
|
+
if not compact:
|
235
|
+
s.write(_indent('', root_indent))
|
236
|
+
s.write(bracket_end)
|
237
|
+
return s.getvalue()
|
238
|
+
|
239
|
+
|
240
|
+
def quote_if_str(value: Any) -> Any:
|
241
|
+
"""Quotes the value if it is a str."""
|
242
|
+
if isinstance(value, str):
|
243
|
+
return repr(value)
|
244
|
+
return value
|
245
|
+
|
246
|
+
|
247
|
+
def comma_delimited_str(value_list: Sequence[Any]) -> str:
|
248
|
+
"""Gets comma delimited string."""
|
249
|
+
return ', '.join(str(quote_if_str(v)) for v in value_list)
|
250
|
+
|
251
|
+
|
252
|
+
def auto_plural(
|
253
|
+
number: int, singular: str, plural: Optional[str] = None) -> str:
|
254
|
+
"""Use singular form if number is 1, otherwise use plural form."""
|
255
|
+
if plural is None:
|
256
|
+
plural = singular + 's'
|
257
|
+
return singular if number == 1 else plural
|
258
|
+
|
259
|
+
|
260
|
+
def format( # pylint: disable=redefined-builtin
|
261
|
+
value: Any,
|
262
|
+
compact: bool = False,
|
263
|
+
verbose: bool = True,
|
264
|
+
root_indent: int = 0,
|
265
|
+
list_wrap_threshold: int = 80,
|
266
|
+
strip_object_id: bool = False,
|
267
|
+
include_keys: Optional[Set[str]] = None,
|
268
|
+
exclude_keys: Optional[Set[str]] = None,
|
269
|
+
markdown: bool = False,
|
270
|
+
max_str_len: Optional[int] = None,
|
271
|
+
max_bytes_len: Optional[int] = None,
|
272
|
+
*,
|
273
|
+
custom_format: Optional[CustomFormatFn] = None,
|
274
|
+
**kwargs,
|
275
|
+
) -> str:
|
276
|
+
"""Formats a (maybe) hierarchical value with flags.
|
277
|
+
|
278
|
+
Args:
|
279
|
+
value: The value to format.
|
280
|
+
compact: If True, this object will be formatted into a single line.
|
281
|
+
verbose: If True, this object will be formatted with verbosity. Subclasses
|
282
|
+
should define `verbosity` on their own.
|
283
|
+
root_indent: The start indent level for this object if the output is a
|
284
|
+
multi-line string.
|
285
|
+
list_wrap_threshold: A threshold in number of characters for wrapping a list
|
286
|
+
value in a single line.
|
287
|
+
strip_object_id: If True, format object as '<class-name>(...)' other than
|
288
|
+
'object at <address>'.
|
289
|
+
include_keys: A set of keys to include from the top-level dict or object.
|
290
|
+
exclude_keys: A set of keys to exclude from the top-level dict or object.
|
291
|
+
Applicable only when `include_keys` is set to None.
|
292
|
+
markdown: If True, use markdown notion to quote the formatted object.
|
293
|
+
max_str_len: The max length of the string to be formatted. If the string is
|
294
|
+
longer than this length, it will be truncated.
|
295
|
+
max_bytes_len: The max length of the bytes to be formatted. If the bytes is
|
296
|
+
longer than this length, it will be truncated.
|
297
|
+
custom_format: An optional custom format function, which will be applied to
|
298
|
+
each value (and child values) in kvlist. If the function returns None, it
|
299
|
+
will fall back to the default `pg.format`.
|
300
|
+
**kwargs: Keyword arguments that will be passed through unto child
|
301
|
+
``Formattable`` objects.
|
302
|
+
|
303
|
+
Returns:
|
304
|
+
A string representation for `value`.
|
305
|
+
"""
|
306
|
+
# We allow custom_format to intercept the entire value if it's present.
|
307
|
+
if custom_format is not None:
|
308
|
+
result = custom_format(value, root_indent)
|
309
|
+
if result is not None:
|
310
|
+
return maybe_markdown_quote(result, markdown)
|
311
|
+
|
312
|
+
exclude_keys = exclude_keys or set()
|
313
|
+
|
314
|
+
def _should_include_key(key: str) -> bool:
|
315
|
+
if include_keys:
|
316
|
+
return key in include_keys
|
317
|
+
return key not in exclude_keys
|
318
|
+
|
319
|
+
def _format_child(v):
|
320
|
+
return format(
|
321
|
+
v,
|
322
|
+
compact=compact,
|
323
|
+
verbose=verbose,
|
324
|
+
root_indent=root_indent + 1,
|
325
|
+
list_wrap_threshold=list_wrap_threshold,
|
326
|
+
strip_object_id=strip_object_id,
|
327
|
+
max_str_len=max_str_len,
|
328
|
+
max_bytes_len=max_bytes_len,
|
329
|
+
custom_format=custom_format,
|
330
|
+
**kwargs
|
331
|
+
)
|
332
|
+
|
333
|
+
# `markdown` is only applied at the outter most level,
|
334
|
+
# so we disable markdown format in the sub-tree for `str` and `repr`.
|
335
|
+
with str_format(markdown=False), repr_format(markdown=False):
|
336
|
+
if isinstance(value, Formattable):
|
337
|
+
s = value.format(
|
338
|
+
compact=compact,
|
339
|
+
verbose=verbose,
|
340
|
+
root_indent=root_indent,
|
341
|
+
list_wrap_threshold=list_wrap_threshold,
|
342
|
+
strip_object_id=strip_object_id,
|
343
|
+
include_keys=include_keys,
|
344
|
+
exclude_keys=exclude_keys,
|
345
|
+
max_str_len=max_str_len,
|
346
|
+
max_bytes_len=max_bytes_len,
|
347
|
+
custom_format=custom_format,
|
348
|
+
**kwargs
|
349
|
+
)
|
350
|
+
elif isinstance(value, (list, tuple)):
|
351
|
+
# Always try compact representation if length is not too long.
|
352
|
+
open_bracket, close_bracket = bracket_chars(
|
353
|
+
BracketType.SQUARE if isinstance(value, list) else BracketType.ROUND)
|
354
|
+
s = [open_bracket]
|
355
|
+
s.append(', '.join([_format_child(elem) for elem in value]))
|
356
|
+
s.append(close_bracket)
|
357
|
+
s = [''.join(s)]
|
358
|
+
if not compact and len(s[-1]) > list_wrap_threshold:
|
359
|
+
s = [f'{open_bracket}\n']
|
360
|
+
s.append(',\n'.join([
|
361
|
+
_indent(_format_child(elem), root_indent + 1)
|
362
|
+
for elem in value
|
363
|
+
]))
|
364
|
+
s.append('\n')
|
365
|
+
s.append(_indent(close_bracket, root_indent))
|
366
|
+
elif isinstance(value, dict):
|
367
|
+
if compact or not value:
|
368
|
+
s = ['{']
|
369
|
+
s.append(', '.join([
|
370
|
+
f'{k!r}: {_format_child(v)}'
|
371
|
+
for k, v in value.items() if _should_include_key(k)
|
372
|
+
]))
|
373
|
+
s.append('}')
|
374
|
+
else:
|
375
|
+
s = ['{\n']
|
376
|
+
s.append(',\n'.join([
|
377
|
+
_indent(f'{k!r}: {_format_child(v)}', root_indent + 1)
|
378
|
+
for k, v in value.items() if _should_include_key(k)
|
379
|
+
]))
|
380
|
+
s.append('\n')
|
381
|
+
s.append(_indent('}', root_indent))
|
382
|
+
else:
|
383
|
+
if isinstance(value, str):
|
384
|
+
if max_str_len is not None and len(value) > max_str_len:
|
385
|
+
value = value[:max_str_len] + '...'
|
386
|
+
s = [repr_ext(value, custom_format, root_indent)]
|
387
|
+
elif isinstance(value, bytes):
|
388
|
+
if max_bytes_len is not None and len(value) > max_bytes_len:
|
389
|
+
value = value[:max_bytes_len] + b'...'
|
390
|
+
s = [repr_ext(value, custom_format, root_indent)]
|
391
|
+
else:
|
392
|
+
s = [repr_ext(value, custom_format, root_indent)
|
393
|
+
if compact else str_ext(value, custom_format, root_indent)]
|
394
|
+
if strip_object_id and 'object at 0x' in s[-1]:
|
395
|
+
s = [f'{value.__class__.__name__}(...)']
|
396
|
+
return maybe_markdown_quote(''.join(s), markdown)
|
397
|
+
|
398
|
+
|
399
|
+
def _maybe_custom_format(
|
400
|
+
v: Any,
|
401
|
+
default_fn: Callable[[Any], str],
|
402
|
+
custom_format: Optional[CustomFormatFn],
|
403
|
+
root_indent: int,
|
404
|
+
) -> str:
|
405
|
+
if custom_format is None:
|
406
|
+
return default_fn(v)
|
407
|
+
x = custom_format(v, root_indent)
|
408
|
+
if x is None:
|
409
|
+
return default_fn(v)
|
410
|
+
return x
|
411
|
+
|
412
|
+
|
413
|
+
def str_ext(
|
414
|
+
v: Any,
|
415
|
+
custom_format: Optional[CustomFormatFn] = None,
|
416
|
+
root_indent: int = 0,
|
417
|
+
) -> str:
|
418
|
+
""""str operator with special format support."""
|
419
|
+
return _maybe_custom_format(v, str, custom_format, root_indent)
|
420
|
+
|
421
|
+
|
422
|
+
def repr_ext(
|
423
|
+
v: Any,
|
424
|
+
custom_format: Optional[CustomFormatFn] = None,
|
425
|
+
root_indent: int = 0,
|
426
|
+
) -> str:
|
427
|
+
"""repr operator with special format support."""
|
428
|
+
return _maybe_custom_format(v, repr, custom_format, root_indent)
|
429
|
+
|
430
|
+
|
431
|
+
def maybe_markdown_quote(s: str, markdown: bool = True) -> str:
|
432
|
+
"""Maybe quote the formatted string with markdown."""
|
433
|
+
if not markdown:
|
434
|
+
return s
|
435
|
+
if '\n' not in s:
|
436
|
+
return f'`{s}`'
|
437
|
+
else:
|
438
|
+
return f'```\n{s}\n```'
|
439
|
+
|
440
|
+
|
441
|
+
def camel_to_snake(text: str, separator: str = '_') -> str:
|
442
|
+
"""Returns the snake case version of a camel case string."""
|
443
|
+
chunks = []
|
444
|
+
chunk_start = 0
|
445
|
+
last_upper = 0
|
446
|
+
length = len(text)
|
447
|
+
for i, c in enumerate(text):
|
448
|
+
if c.isupper():
|
449
|
+
if last_upper < i - 1 or (i < length - 1 and text[i + 1].islower()):
|
450
|
+
chunks.append(text[chunk_start:i])
|
451
|
+
chunk_start = i
|
452
|
+
last_upper = i
|
453
|
+
chunks.append(text[chunk_start:])
|
454
|
+
return (separator.join(c for c in chunks if c)).lower()
|
455
|
+
|
456
|
+
|
457
|
+
def printv(v: Any, **kwargs):
|
458
|
+
"""Prints formatted value."""
|
459
|
+
fs = kwargs.pop('file', sys.stdout)
|
460
|
+
print(format(v, **kwargs), file=fs)
|
461
|
+
|
462
|
+
|
463
|
+
def _indent(text: str, indent: int) -> str:
|
464
|
+
return ' ' * 2 * indent + text
|