pythonic-peano-arithmetic 0.3.0__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.
- peano/__init__.py +66 -0
- peano/algebraic_root.py +180 -0
- peano/integer.py +345 -0
- peano/natural_number.py +340 -0
- peano/polynomial.py +431 -0
- peano/py.typed +1 -0
- peano/rational.py +295 -0
- peano/utils.py +189 -0
- pythonic_peano_arithmetic-0.3.0.dist-info/METADATA +240 -0
- pythonic_peano_arithmetic-0.3.0.dist-info/RECORD +12 -0
- pythonic_peano_arithmetic-0.3.0.dist-info/WHEEL +4 -0
- pythonic_peano_arithmetic-0.3.0.dist-info/licenses/LICENSE +21 -0
peano/rational.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Construct rational numbers as equivalence classes of integer ratios."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import total_ordering
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from .integer import Z_ONE, Z_ZERO, Integer, integer, n2z
|
|
10
|
+
from .natural_number import N_ONE, N_ZERO, NaturalNumber
|
|
11
|
+
from .utils import LogMessage, log
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@total_ordering
|
|
15
|
+
@dataclass(frozen=True, slots=True, eq=False, repr=False)
|
|
16
|
+
class Rational:
|
|
17
|
+
"""Represent a rational number as an integer ratio ``p / q``, ``q != 0``.
|
|
18
|
+
|
|
19
|
+
``p/q ~ r/s`` is defined by the cross products ``p*s = q*r``. Input
|
|
20
|
+
representatives are preserved; ``reduction`` normalizes the denominator
|
|
21
|
+
and reduces the ratio only when explicitly requested.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
p: Integer
|
|
25
|
+
q: Integer
|
|
26
|
+
|
|
27
|
+
def __post_init__(self) -> None:
|
|
28
|
+
if not isinstance(self.p, Integer) or not isinstance(self.q, Integer):
|
|
29
|
+
raise TypeError("Rational.p and Rational.q must be Integer values")
|
|
30
|
+
if self.q == Z_ZERO:
|
|
31
|
+
raise ZeroDivisionError("the denominator cannot be zero")
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"<Q({self})>"
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
return f"{self.p}/{self.q}"
|
|
38
|
+
|
|
39
|
+
@log(log_level=21)
|
|
40
|
+
def __eq__(self, other: object) -> tuple[bool, LogMessage]:
|
|
41
|
+
converted = _coerce_rational(other)
|
|
42
|
+
if converted is None:
|
|
43
|
+
return (
|
|
44
|
+
cast(bool, NotImplemented),
|
|
45
|
+
lambda: f"{self!r} == {other!r} = NotImplemented",
|
|
46
|
+
)
|
|
47
|
+
result = self.p * converted.q == self.q * converted.p
|
|
48
|
+
return (
|
|
49
|
+
result,
|
|
50
|
+
lambda: (
|
|
51
|
+
f"{self!r} == {converted!r} ⇔ "
|
|
52
|
+
f"{self.p!r} * {converted.q!r} == "
|
|
53
|
+
f"{self.q!r} * {converted.p!r}"
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@log(log_level=22)
|
|
58
|
+
def __lt__(self, other: object) -> tuple[bool, LogMessage]:
|
|
59
|
+
converted = _coerce_rational(other)
|
|
60
|
+
if converted is None:
|
|
61
|
+
return (
|
|
62
|
+
cast(bool, NotImplemented),
|
|
63
|
+
lambda: f"{self!r} < {other!r} = NotImplemented",
|
|
64
|
+
)
|
|
65
|
+
left = self.p * converted.q
|
|
66
|
+
right = self.q * converted.p
|
|
67
|
+
denominators_have_different_signs = (self.q < Z_ZERO) != (converted.q < Z_ZERO)
|
|
68
|
+
result = left > right if denominators_have_different_signs else left < right
|
|
69
|
+
operator = ">" if denominators_have_different_signs else "<"
|
|
70
|
+
return (
|
|
71
|
+
result,
|
|
72
|
+
lambda: f"{self!r} < {converted!r} ⇔ {left!r} {operator} {right!r}",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@log(log_level=22)
|
|
76
|
+
def __le__(self, other: object) -> tuple[bool, LogMessage]:
|
|
77
|
+
converted = _coerce_rational(other)
|
|
78
|
+
if converted is None:
|
|
79
|
+
return (
|
|
80
|
+
cast(bool, NotImplemented),
|
|
81
|
+
lambda: f"{self!r} <= {other!r} = NotImplemented",
|
|
82
|
+
)
|
|
83
|
+
left = self.p * converted.q
|
|
84
|
+
right = self.q * converted.p
|
|
85
|
+
denominators_have_different_signs = (self.q < Z_ZERO) != (converted.q < Z_ZERO)
|
|
86
|
+
result = left >= right if denominators_have_different_signs else left <= right
|
|
87
|
+
operator = ">=" if denominators_have_different_signs else "<="
|
|
88
|
+
return (
|
|
89
|
+
result,
|
|
90
|
+
lambda: f"{self!r} <= {converted!r} ⇔ {left!r} {operator} {right!r}",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
@log(log_level=24)
|
|
94
|
+
def __add__(self, other: object) -> tuple[Rational, LogMessage]:
|
|
95
|
+
converted = _coerce_rational(other)
|
|
96
|
+
if converted is None:
|
|
97
|
+
return (
|
|
98
|
+
cast(Rational, NotImplemented),
|
|
99
|
+
lambda: f"{self!r} + {other!r} = NotImplemented",
|
|
100
|
+
)
|
|
101
|
+
result = Rational(
|
|
102
|
+
self.p * converted.q + self.q * converted.p,
|
|
103
|
+
self.q * converted.q,
|
|
104
|
+
)
|
|
105
|
+
return (
|
|
106
|
+
result,
|
|
107
|
+
lambda: (
|
|
108
|
+
f"{self!r} + {converted!r} = "
|
|
109
|
+
f"({self.p!r} * {converted.q!r} + "
|
|
110
|
+
f"{self.q!r} * {converted.p!r}) / "
|
|
111
|
+
f"({self.q!r} * {converted.q!r})"
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def __radd__(self, other: object) -> Rational:
|
|
116
|
+
return self + other
|
|
117
|
+
|
|
118
|
+
@log(log_level=24)
|
|
119
|
+
def __neg__(self) -> tuple[Rational, LogMessage]:
|
|
120
|
+
result = Rational(-self.p, self.q)
|
|
121
|
+
return result, lambda: f"-{self!r} = (-{self.p!r}) / {self.q!r}"
|
|
122
|
+
|
|
123
|
+
@log(log_level=24)
|
|
124
|
+
def __sub__(self, other: object) -> tuple[Rational, LogMessage]:
|
|
125
|
+
converted = _coerce_rational(other)
|
|
126
|
+
if converted is None:
|
|
127
|
+
return (
|
|
128
|
+
cast(Rational, NotImplemented),
|
|
129
|
+
lambda: f"{self!r} - {other!r} = NotImplemented",
|
|
130
|
+
)
|
|
131
|
+
return (
|
|
132
|
+
self + -converted,
|
|
133
|
+
lambda: f"{self!r} - {converted!r} = {self!r} + (-{converted!r})",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def __rsub__(self, other: object) -> Rational:
|
|
137
|
+
converted = _coerce_rational(other)
|
|
138
|
+
if converted is None:
|
|
139
|
+
return cast(Rational, NotImplemented)
|
|
140
|
+
return converted - self
|
|
141
|
+
|
|
142
|
+
@log(log_level=25)
|
|
143
|
+
def __mul__(self, other: object) -> tuple[Rational, LogMessage]:
|
|
144
|
+
converted = _coerce_rational(other)
|
|
145
|
+
if converted is None:
|
|
146
|
+
return (
|
|
147
|
+
cast(Rational, NotImplemented),
|
|
148
|
+
lambda: f"{self!r} * {other!r} = NotImplemented",
|
|
149
|
+
)
|
|
150
|
+
result = Rational(self.p * converted.p, self.q * converted.q)
|
|
151
|
+
return (
|
|
152
|
+
result,
|
|
153
|
+
lambda: (
|
|
154
|
+
f"{self!r} * {converted!r} = "
|
|
155
|
+
f"({self.p!r} * {converted.p!r}) / "
|
|
156
|
+
f"({self.q!r} * {converted.q!r})"
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def __rmul__(self, other: object) -> Rational:
|
|
161
|
+
return self * other
|
|
162
|
+
|
|
163
|
+
@log(log_level=25)
|
|
164
|
+
def __truediv__(self, other: object) -> tuple[Rational, LogMessage]:
|
|
165
|
+
converted = _coerce_rational(other)
|
|
166
|
+
if converted is None:
|
|
167
|
+
return (
|
|
168
|
+
cast(Rational, NotImplemented),
|
|
169
|
+
lambda: f"{self!r} / {other!r} = NotImplemented",
|
|
170
|
+
)
|
|
171
|
+
if not converted:
|
|
172
|
+
raise ZeroDivisionError("division by zero")
|
|
173
|
+
result = Rational(self.p * converted.q, self.q * converted.p)
|
|
174
|
+
return (
|
|
175
|
+
result,
|
|
176
|
+
lambda: (
|
|
177
|
+
f"{self!r} / {converted!r} = "
|
|
178
|
+
f"({self.p!r} * {converted.q!r}) / "
|
|
179
|
+
f"({self.q!r} * {converted.p!r})"
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def __rtruediv__(self, other: object) -> Rational:
|
|
184
|
+
converted = _coerce_rational(other)
|
|
185
|
+
if converted is None:
|
|
186
|
+
return cast(Rational, NotImplemented)
|
|
187
|
+
return converted / self
|
|
188
|
+
|
|
189
|
+
@log(log_level=26)
|
|
190
|
+
def __pow__(self, exponent: object) -> tuple[Rational, LogMessage]:
|
|
191
|
+
if not isinstance(exponent, NaturalNumber):
|
|
192
|
+
return (
|
|
193
|
+
cast(Rational, NotImplemented),
|
|
194
|
+
lambda: f"{self!r} ** {exponent!r} = NotImplemented",
|
|
195
|
+
)
|
|
196
|
+
if exponent == N_ZERO:
|
|
197
|
+
return Q_ONE, lambda: f"{self!r} ** {exponent!r} = {Q_ONE!r}"
|
|
198
|
+
return (
|
|
199
|
+
(self ** (exponent - N_ONE)) * self,
|
|
200
|
+
lambda: (
|
|
201
|
+
f"{self!r} ** {exponent!r} = "
|
|
202
|
+
f"({self!r} ** ({exponent!r} - {N_ONE!r})) * {self!r}"
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def __bool__(self) -> bool:
|
|
207
|
+
return self.p != Z_ZERO
|
|
208
|
+
|
|
209
|
+
def __hash__(self) -> int:
|
|
210
|
+
reduced = self.reduction()
|
|
211
|
+
if reduced.q == Z_ONE:
|
|
212
|
+
# Equal NaturalNumber and Integer values must share the same hash.
|
|
213
|
+
return hash(reduced.p)
|
|
214
|
+
return hash(("Rational", int(reduced.p), int(reduced.q)))
|
|
215
|
+
|
|
216
|
+
def __pos__(self) -> Rational:
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def __abs__(self) -> Rational:
|
|
220
|
+
return Rational(Integer(abs(self.p), N_ZERO), Integer(abs(self.q), N_ZERO))
|
|
221
|
+
|
|
222
|
+
def as_integer_ratio(self) -> tuple[int, int]:
|
|
223
|
+
"""Return a reduced Python integer ratio with a positive denominator.
|
|
224
|
+
|
|
225
|
+
This boundary API avoids constructing equivalent but enormous Peano
|
|
226
|
+
intermediates when displaying or validating large rational values.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
from math import gcd
|
|
230
|
+
|
|
231
|
+
numerator, denominator = int(self.p), int(self.q)
|
|
232
|
+
if denominator < 0:
|
|
233
|
+
numerator, denominator = -numerator, -denominator
|
|
234
|
+
divisor = gcd(abs(numerator), denominator)
|
|
235
|
+
return numerator // divisor, denominator // divisor
|
|
236
|
+
|
|
237
|
+
def reduction(self) -> Rational:
|
|
238
|
+
"""Return a reduced representative with a positive denominator."""
|
|
239
|
+
|
|
240
|
+
numerator = self.p.normalize()
|
|
241
|
+
denominator = self.q.normalize()
|
|
242
|
+
if denominator < Z_ZERO:
|
|
243
|
+
numerator, denominator = -numerator, -denominator
|
|
244
|
+
|
|
245
|
+
a, b = abs(numerator), abs(denominator)
|
|
246
|
+
while b:
|
|
247
|
+
a, b = b, a % b
|
|
248
|
+
divisor = Integer(a, N_ZERO)
|
|
249
|
+
return Rational(numerator // divisor, denominator // divisor)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def rational(numerator: int, denominator: int) -> Rational:
|
|
253
|
+
"""Construct a rational number from two Python integers."""
|
|
254
|
+
|
|
255
|
+
return Rational(integer(numerator), integer(denominator))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def n2r(value: NaturalNumber) -> Rational:
|
|
259
|
+
"""Embed a natural number into the rationals."""
|
|
260
|
+
|
|
261
|
+
if not isinstance(value, NaturalNumber):
|
|
262
|
+
raise TypeError("n2r expects a NaturalNumber")
|
|
263
|
+
return Rational(n2z(value), Z_ONE)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def z2r(value: Integer) -> Rational:
|
|
267
|
+
"""Embed an integer into the rationals."""
|
|
268
|
+
|
|
269
|
+
if not isinstance(value, Integer):
|
|
270
|
+
raise TypeError("z2r expects an Integer")
|
|
271
|
+
return Rational(value, Z_ONE)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _coerce_rational(value: object) -> Rational | None:
|
|
275
|
+
if isinstance(value, NaturalNumber):
|
|
276
|
+
return n2r(value)
|
|
277
|
+
if isinstance(value, Integer):
|
|
278
|
+
return z2r(value)
|
|
279
|
+
if isinstance(value, Rational):
|
|
280
|
+
return value
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def cast2r(value: object) -> Rational:
|
|
285
|
+
"""Coerce a supported numeric value to ``Rational``."""
|
|
286
|
+
|
|
287
|
+
converted = _coerce_rational(value)
|
|
288
|
+
if converted is None:
|
|
289
|
+
raise TypeError(f"{value!r} is not a Rational")
|
|
290
|
+
return converted
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
Q_ZERO = Rational(Z_ZERO, Z_ONE)
|
|
294
|
+
Q_ONE = Rational(Z_ONE, Z_ONE)
|
|
295
|
+
Q_MINUS_ONE = Rational(-Z_ONE, Z_ONE)
|
peano/utils.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Lightweight logging support for observing symbolic evaluation."""
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from inspect import signature
|
|
6
|
+
from logging import (
|
|
7
|
+
Filter,
|
|
8
|
+
Formatter,
|
|
9
|
+
Logger,
|
|
10
|
+
LogRecord,
|
|
11
|
+
NullHandler,
|
|
12
|
+
StreamHandler,
|
|
13
|
+
getLogger,
|
|
14
|
+
)
|
|
15
|
+
from typing import Callable, Literal, ParamSpec, TypeVar, get_args, get_origin
|
|
16
|
+
|
|
17
|
+
logger = getLogger(__name__)
|
|
18
|
+
logger.addHandler(NullHandler())
|
|
19
|
+
logger.propagate = False
|
|
20
|
+
logger.disabled = True
|
|
21
|
+
|
|
22
|
+
P = ParamSpec("P")
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
LogMessage = str | Callable[[], str]
|
|
25
|
+
_PEANO_HANDLER_MARKER = "_peano_handler"
|
|
26
|
+
_LOG_LIMIT_NOTICE = "_peano_log_limit_notice"
|
|
27
|
+
_locale: Literal["en", "ja"] = "en"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def localized(english: str, japanese: str) -> str:
|
|
31
|
+
"""Return a localized runtime message."""
|
|
32
|
+
|
|
33
|
+
return japanese if _locale == "ja" else english
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _LineLimitFormatter(Formatter):
|
|
37
|
+
"""Format a copied record when emitting the truncation notice."""
|
|
38
|
+
|
|
39
|
+
def format(self, record: LogRecord) -> str:
|
|
40
|
+
max_lines = getattr(record, _LOG_LIMIT_NOTICE, None)
|
|
41
|
+
if not isinstance(max_lines, int):
|
|
42
|
+
return super().format(record)
|
|
43
|
+
notice = copy(record)
|
|
44
|
+
notice.msg = localized(
|
|
45
|
+
f"…Log output was truncated after {max_lines} lines. "
|
|
46
|
+
"Use smaller inputs and run the cell again.",
|
|
47
|
+
f"…ログは{max_lines}行で省略しました。入力を小さくして再実行してください。",
|
|
48
|
+
)
|
|
49
|
+
notice.args = ()
|
|
50
|
+
return super().format(notice)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _LineLimitFilter(Filter):
|
|
54
|
+
"""Emit at most ``max_lines`` messages plus one truncation notice."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, max_lines: int | None) -> None:
|
|
57
|
+
super().__init__(__name__)
|
|
58
|
+
self.max_lines = max_lines
|
|
59
|
+
self.emitted_lines = 0
|
|
60
|
+
|
|
61
|
+
def filter(self, record: LogRecord) -> bool:
|
|
62
|
+
if not super().filter(record):
|
|
63
|
+
return False
|
|
64
|
+
if self.max_lines is None:
|
|
65
|
+
return True
|
|
66
|
+
if self.emitted_lines < self.max_lines:
|
|
67
|
+
self.emitted_lines += 1
|
|
68
|
+
return True
|
|
69
|
+
if self.emitted_lines == self.max_lines:
|
|
70
|
+
self.emitted_lines += 1
|
|
71
|
+
setattr(record, _LOG_LIMIT_NOTICE, self.max_lines)
|
|
72
|
+
return True
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def log(
|
|
77
|
+
log_level: int,
|
|
78
|
+
) -> Callable[[Callable[P, tuple[T, LogMessage]]], Callable[P, T]]:
|
|
79
|
+
"""Turn an operation returning a result and message into a public operation.
|
|
80
|
+
|
|
81
|
+
The implementation returns ``(result, formula)``. Callers receive only
|
|
82
|
+
``result`` while ``formula`` is logged at the requested level. Callable
|
|
83
|
+
messages are evaluated lazily only when logging is enabled.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def outer(func: Callable[P, tuple[T, LogMessage]]) -> Callable[P, T]:
|
|
87
|
+
@wraps(func)
|
|
88
|
+
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
89
|
+
result, message = func(*args, **kwargs)
|
|
90
|
+
if logger.isEnabledFor(log_level):
|
|
91
|
+
logger.log(
|
|
92
|
+
log_level, message if isinstance(message, str) else message()
|
|
93
|
+
)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
public_return = _public_return_annotation(
|
|
97
|
+
signature(func, follow_wrapped=False).return_annotation
|
|
98
|
+
)
|
|
99
|
+
inner.__annotations__ = {
|
|
100
|
+
**func.__annotations__,
|
|
101
|
+
"return": public_return,
|
|
102
|
+
}
|
|
103
|
+
setattr(
|
|
104
|
+
inner,
|
|
105
|
+
"__signature__",
|
|
106
|
+
signature(func, follow_wrapped=False).replace(
|
|
107
|
+
return_annotation=public_return
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
return inner
|
|
111
|
+
|
|
112
|
+
return outer
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _public_return_annotation(annotation: object) -> object:
|
|
116
|
+
"""Extract the public result type from the internal tuple annotation."""
|
|
117
|
+
|
|
118
|
+
if isinstance(annotation, str):
|
|
119
|
+
prefix = "tuple["
|
|
120
|
+
for suffix in (", str]", ", LogMessage]"):
|
|
121
|
+
if annotation.startswith(prefix) and annotation.endswith(suffix):
|
|
122
|
+
return annotation[len(prefix) : -len(suffix)]
|
|
123
|
+
elif get_origin(annotation) is tuple:
|
|
124
|
+
arguments = get_args(annotation)
|
|
125
|
+
if len(arguments) == 2 and arguments[1] in (str, LogMessage):
|
|
126
|
+
return arguments[0]
|
|
127
|
+
raise TypeError("functions decorated with log must return (result, LogMessage)")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _remove_peano_handlers(*loggers: Logger) -> None:
|
|
131
|
+
"""Remove only handlers installed by this module."""
|
|
132
|
+
|
|
133
|
+
for target in loggers:
|
|
134
|
+
for handler in tuple(target.handlers):
|
|
135
|
+
if getattr(handler, _PEANO_HANDLER_MARKER, False):
|
|
136
|
+
target.removeHandler(handler)
|
|
137
|
+
handler.close()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def config_log(
|
|
141
|
+
log_level: int = 0,
|
|
142
|
+
root: bool = False,
|
|
143
|
+
fmt: str = "%(message)s",
|
|
144
|
+
clear_handlers: bool = True,
|
|
145
|
+
max_lines: int | None = None,
|
|
146
|
+
locale: Literal["en", "ja"] = "en",
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Write symbolic evaluation logs to standard error.
|
|
149
|
+
|
|
150
|
+
By default, only the library logger is configured. ``root=True`` preserves
|
|
151
|
+
existing root handlers and manages only the handler installed here.
|
|
152
|
+
``max_lines`` emits one truncation notice after the limit. Pass
|
|
153
|
+
``"Level %(levelno)s: %(message)s"`` as ``fmt`` to expose internal levels.
|
|
154
|
+
Runtime labels are English by default; ``locale="ja"`` selects Japanese.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
global _locale
|
|
158
|
+
if max_lines is not None and (
|
|
159
|
+
isinstance(max_lines, bool) or not isinstance(max_lines, int) or max_lines < 1
|
|
160
|
+
):
|
|
161
|
+
raise ValueError("max_lines must be a positive integer or None")
|
|
162
|
+
if locale not in ("en", "ja"):
|
|
163
|
+
raise ValueError("locale must be 'en' or 'ja'")
|
|
164
|
+
_locale = locale
|
|
165
|
+
|
|
166
|
+
root_logger = getLogger()
|
|
167
|
+
if clear_handlers:
|
|
168
|
+
_remove_peano_handlers(logger, root_logger)
|
|
169
|
+
|
|
170
|
+
handler = StreamHandler()
|
|
171
|
+
handler.setFormatter(_LineLimitFormatter(fmt))
|
|
172
|
+
handler.setLevel(log_level)
|
|
173
|
+
handler.addFilter(_LineLimitFilter(max_lines))
|
|
174
|
+
setattr(handler, _PEANO_HANDLER_MARKER, True)
|
|
175
|
+
|
|
176
|
+
logger.setLevel(log_level)
|
|
177
|
+
logger.disabled = False
|
|
178
|
+
if root:
|
|
179
|
+
logger.propagate = True
|
|
180
|
+
root_logger.addHandler(handler)
|
|
181
|
+
else:
|
|
182
|
+
logger.propagate = False
|
|
183
|
+
logger.addHandler(handler)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def explain(value: object) -> str:
|
|
187
|
+
"""Render a value consistently inside symbolic log messages."""
|
|
188
|
+
|
|
189
|
+
return repr(value)
|