pythonic-peano-arithmetic 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/PKG-INFO +20 -8
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/README.md +19 -7
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/peano/__init__.py +2 -1
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/peano/algebraic_root.py +5 -4
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/peano/natural_number.py +7 -7
- pythonic_peano_arithmetic-0.4.0/peano/utils.py +372 -0
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/pyproject.toml +1 -1
- pythonic_peano_arithmetic-0.3.0/peano/utils.py +0 -189
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/LICENSE +0 -0
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/peano/integer.py +0 -0
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/peano/polynomial.py +0 -0
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/peano/py.typed +0 -0
- {pythonic_peano_arithmetic-0.3.0 → pythonic_peano_arithmetic-0.4.0}/peano/rational.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonic-peano-arithmetic
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Build arithmetic from Peano axioms and trace algebraic real roots
|
|
5
5
|
Keywords: arithmetic,education,mathematics,peano
|
|
6
6
|
Author: Yusuke Hayashi
|
|
@@ -35,10 +35,20 @@ The implementation is intentionally small and explicit. Its purpose is to make
|
|
|
35
35
|
the correspondence between a mathematical definition and executable Python
|
|
36
36
|
visible—not to compete with Python's built-in numeric types.
|
|
37
37
|
|
|
38
|
-
The interactive course runs entirely in the browser with Zensical and Pyodide
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
The interactive course runs entirely in the browser with Zensical and Pyodide.
|
|
39
|
+
It is available in
|
|
40
|
+
[English](https://peano.yusuke-hayashi.com/en/),
|
|
41
|
+
[日本語](https://peano.yusuke-hayashi.com/),
|
|
42
|
+
[简体中文](https://peano.yusuke-hayashi.com/zh/),
|
|
43
|
+
[繁體中文](https://peano.yusuke-hayashi.com/zh-hant/),
|
|
44
|
+
[Español](https://peano.yusuke-hayashi.com/es/),
|
|
45
|
+
[Português (Brasil)](https://peano.yusuke-hayashi.com/pt-br/),
|
|
46
|
+
[Français](https://peano.yusuke-hayashi.com/fr/),
|
|
47
|
+
[Deutsch](https://peano.yusuke-hayashi.com/de/),
|
|
48
|
+
[한국어](https://peano.yusuke-hayashi.com/ko/),
|
|
49
|
+
[Русский](https://peano.yusuke-hayashi.com/ru/),
|
|
50
|
+
[العربية](https://peano.yusuke-hayashi.com/ar/), and
|
|
51
|
+
[हिन्दी](https://peano.yusuke-hayashi.com/hi/).
|
|
42
52
|
|
|
43
53
|
## What you can observe
|
|
44
54
|
|
|
@@ -98,7 +108,9 @@ n + 0 = n
|
|
|
98
108
|
n + S(m) = S(n + m)
|
|
99
109
|
```
|
|
100
110
|
|
|
101
|
-
Pass
|
|
111
|
+
Pass a BCP 47 locale to `config_log` to localize rule labels. Supported values
|
|
112
|
+
are `en`, `ja`, `zh-Hans`, `zh-Hant`, `es`, `pt-BR`, `fr`, `de`, `ko`, `ru`,
|
|
113
|
+
`ar`, and `hi`.
|
|
102
114
|
|
|
103
115
|
### Integers: equality of representatives
|
|
104
116
|
|
|
@@ -220,10 +232,10 @@ intermediate Peano representations; returned endpoints are still `Rational`.
|
|
|
220
232
|
## Documentation development
|
|
221
233
|
|
|
222
234
|
```bash
|
|
223
|
-
make docs # build
|
|
235
|
+
make docs # build all 12 course languages
|
|
224
236
|
make docs-serve # preview Japanese
|
|
225
237
|
make docs-serve-en # preview English
|
|
226
|
-
make docs-a11y # run WCAG checks on
|
|
238
|
+
make docs-a11y # run WCAG checks on all 120 localized pages
|
|
227
239
|
```
|
|
228
240
|
|
|
229
241
|
The site is deployed from `main` to Cloudflare Workers Static Assets. Deployment
|
|
@@ -8,10 +8,20 @@ The implementation is intentionally small and explicit. Its purpose is to make
|
|
|
8
8
|
the correspondence between a mathematical definition and executable Python
|
|
9
9
|
visible—not to compete with Python's built-in numeric types.
|
|
10
10
|
|
|
11
|
-
The interactive course runs entirely in the browser with Zensical and Pyodide
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
The interactive course runs entirely in the browser with Zensical and Pyodide.
|
|
12
|
+
It is available in
|
|
13
|
+
[English](https://peano.yusuke-hayashi.com/en/),
|
|
14
|
+
[日本語](https://peano.yusuke-hayashi.com/),
|
|
15
|
+
[简体中文](https://peano.yusuke-hayashi.com/zh/),
|
|
16
|
+
[繁體中文](https://peano.yusuke-hayashi.com/zh-hant/),
|
|
17
|
+
[Español](https://peano.yusuke-hayashi.com/es/),
|
|
18
|
+
[Português (Brasil)](https://peano.yusuke-hayashi.com/pt-br/),
|
|
19
|
+
[Français](https://peano.yusuke-hayashi.com/fr/),
|
|
20
|
+
[Deutsch](https://peano.yusuke-hayashi.com/de/),
|
|
21
|
+
[한국어](https://peano.yusuke-hayashi.com/ko/),
|
|
22
|
+
[Русский](https://peano.yusuke-hayashi.com/ru/),
|
|
23
|
+
[العربية](https://peano.yusuke-hayashi.com/ar/), and
|
|
24
|
+
[हिन्दी](https://peano.yusuke-hayashi.com/hi/).
|
|
15
25
|
|
|
16
26
|
## What you can observe
|
|
17
27
|
|
|
@@ -71,7 +81,9 @@ n + 0 = n
|
|
|
71
81
|
n + S(m) = S(n + m)
|
|
72
82
|
```
|
|
73
83
|
|
|
74
|
-
Pass
|
|
84
|
+
Pass a BCP 47 locale to `config_log` to localize rule labels. Supported values
|
|
85
|
+
are `en`, `ja`, `zh-Hans`, `zh-Hant`, `es`, `pt-BR`, `fr`, `de`, `ko`, `ru`,
|
|
86
|
+
`ar`, and `hi`.
|
|
75
87
|
|
|
76
88
|
### Integers: equality of representatives
|
|
77
89
|
|
|
@@ -193,10 +205,10 @@ intermediate Peano representations; returned endpoints are still `Rational`.
|
|
|
193
205
|
## Documentation development
|
|
194
206
|
|
|
195
207
|
```bash
|
|
196
|
-
make docs # build
|
|
208
|
+
make docs # build all 12 course languages
|
|
197
209
|
make docs-serve # preview Japanese
|
|
198
210
|
make docs-serve-en # preview English
|
|
199
|
-
make docs-a11y # run WCAG checks on
|
|
211
|
+
make docs-a11y # run WCAG checks on all 120 localized pages
|
|
200
212
|
```
|
|
201
213
|
|
|
202
214
|
The site is deployed from `main` to Cloudflare Workers Static Assets. Deployment
|
|
@@ -28,7 +28,7 @@ from .rational import (
|
|
|
28
28
|
rational,
|
|
29
29
|
z2r,
|
|
30
30
|
)
|
|
31
|
-
from .utils import config_log
|
|
31
|
+
from .utils import SUPPORTED_LOCALES, config_log
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
"NaturalNumber",
|
|
@@ -63,4 +63,5 @@ __all__ = [
|
|
|
63
63
|
"AlgebraicRoot",
|
|
64
64
|
"algebraic_root",
|
|
65
65
|
"config_log",
|
|
66
|
+
"SUPPORTED_LOCALES",
|
|
66
67
|
]
|
|
@@ -8,7 +8,7 @@ from fractions import Fraction
|
|
|
8
8
|
from .natural_number import NaturalNumber
|
|
9
9
|
from .polynomial import Polynomial, count_real_roots
|
|
10
10
|
from .rational import Rational, rational
|
|
11
|
-
from .utils import LogMessage,
|
|
11
|
+
from .utils import LogMessage, log, translate
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@dataclass(frozen=True, slots=True)
|
|
@@ -132,9 +132,10 @@ def _bisect(
|
|
|
132
132
|
result = RationalInterval(midpoint, midpoint)
|
|
133
133
|
return (
|
|
134
134
|
result,
|
|
135
|
-
lambda:
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
lambda: translate(
|
|
136
|
+
"midpoint_root",
|
|
137
|
+
polynomial=repr(polynomial_value),
|
|
138
|
+
midpoint=repr(midpoint),
|
|
138
139
|
),
|
|
139
140
|
)
|
|
140
141
|
|
|
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|
|
6
6
|
from functools import total_ordering
|
|
7
7
|
from typing import TYPE_CHECKING, Iterator, cast
|
|
8
8
|
|
|
9
|
-
from .utils import LogMessage,
|
|
9
|
+
from .utils import LogMessage, log, translate
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from .integer import Integer
|
|
@@ -67,7 +67,7 @@ class NaturalNumber:
|
|
|
67
67
|
return (
|
|
68
68
|
result,
|
|
69
69
|
lambda: (
|
|
70
|
-
f"{
|
|
70
|
+
f"{translate('equality.zero')} "
|
|
71
71
|
f"eq({self.structural_str()}, "
|
|
72
72
|
f"{other.structural_str()}) -> {result}"
|
|
73
73
|
),
|
|
@@ -75,7 +75,7 @@ class NaturalNumber:
|
|
|
75
75
|
return (
|
|
76
76
|
left_predecessor == right_predecessor,
|
|
77
77
|
lambda: (
|
|
78
|
-
f"{
|
|
78
|
+
f"{translate('equality.successor')} "
|
|
79
79
|
f"eq({self.structural_str()}, "
|
|
80
80
|
f"{other.structural_str()}) -> "
|
|
81
81
|
f"eq({left_predecessor.structural_str()}, "
|
|
@@ -128,7 +128,7 @@ class NaturalNumber:
|
|
|
128
128
|
return (
|
|
129
129
|
self,
|
|
130
130
|
lambda: (
|
|
131
|
-
f"{
|
|
131
|
+
f"{translate('addition.base')} "
|
|
132
132
|
f"add({self.structural_str()}, 0) "
|
|
133
133
|
f"-> {self.structural_str()}"
|
|
134
134
|
),
|
|
@@ -136,7 +136,7 @@ class NaturalNumber:
|
|
|
136
136
|
return (
|
|
137
137
|
successor(self + predecessor),
|
|
138
138
|
lambda: (
|
|
139
|
-
f"{
|
|
139
|
+
f"{translate('addition.recursive')} "
|
|
140
140
|
f"add({self.structural_str()}, "
|
|
141
141
|
f"{other.structural_str()}) -> "
|
|
142
142
|
f"S(add({self.structural_str()}, {predecessor.structural_str()}))"
|
|
@@ -173,14 +173,14 @@ class NaturalNumber:
|
|
|
173
173
|
return (
|
|
174
174
|
N_ZERO,
|
|
175
175
|
lambda: (
|
|
176
|
-
f"{
|
|
176
|
+
f"{translate('multiplication.base')} "
|
|
177
177
|
f"mul({self.structural_str()}, 0) -> 0"
|
|
178
178
|
),
|
|
179
179
|
)
|
|
180
180
|
return (
|
|
181
181
|
self + self * predecessor,
|
|
182
182
|
lambda: (
|
|
183
|
-
f"{
|
|
183
|
+
f"{translate('multiplication.recursive')} "
|
|
184
184
|
f"mul({self.structural_str()}, "
|
|
185
185
|
f"{other.structural_str()}) -> "
|
|
186
186
|
f"add({self.structural_str()}, "
|
|
@@ -0,0 +1,372 @@
|
|
|
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
|
+
Locale = Literal[
|
|
26
|
+
"en",
|
|
27
|
+
"ja",
|
|
28
|
+
"zh-Hans",
|
|
29
|
+
"zh-Hant",
|
|
30
|
+
"es",
|
|
31
|
+
"pt-BR",
|
|
32
|
+
"fr",
|
|
33
|
+
"de",
|
|
34
|
+
"ko",
|
|
35
|
+
"ru",
|
|
36
|
+
"ar",
|
|
37
|
+
"hi",
|
|
38
|
+
]
|
|
39
|
+
SUPPORTED_LOCALES: tuple[Locale, ...] = (
|
|
40
|
+
"en",
|
|
41
|
+
"ja",
|
|
42
|
+
"zh-Hans",
|
|
43
|
+
"zh-Hant",
|
|
44
|
+
"es",
|
|
45
|
+
"pt-BR",
|
|
46
|
+
"fr",
|
|
47
|
+
"de",
|
|
48
|
+
"ko",
|
|
49
|
+
"ru",
|
|
50
|
+
"ar",
|
|
51
|
+
"hi",
|
|
52
|
+
)
|
|
53
|
+
_PEANO_HANDLER_MARKER = "_peano_handler"
|
|
54
|
+
_LOG_LIMIT_NOTICE = "_peano_log_limit_notice"
|
|
55
|
+
_locale: Locale = "en"
|
|
56
|
+
|
|
57
|
+
_MESSAGES: dict[Locale, dict[str, str]] = {
|
|
58
|
+
"en": {
|
|
59
|
+
"equality.zero": "[equality: zero case]",
|
|
60
|
+
"equality.successor": "[equality: successor case]",
|
|
61
|
+
"addition.base": "[addition: base]",
|
|
62
|
+
"addition.recursive": "[addition: recursive]",
|
|
63
|
+
"multiplication.base": "[multiplication: base]",
|
|
64
|
+
"multiplication.recursive": "[multiplication: recursive]",
|
|
65
|
+
"truncated": (
|
|
66
|
+
"…Log output was truncated after {max_lines} lines. "
|
|
67
|
+
"Use smaller inputs and run the cell again."
|
|
68
|
+
),
|
|
69
|
+
"midpoint_root": "{polynomial}: midpoint {midpoint} is a root",
|
|
70
|
+
},
|
|
71
|
+
"ja": {
|
|
72
|
+
"equality.zero": "[等値・0の場合]",
|
|
73
|
+
"equality.successor": "[等値・後者の場合]",
|
|
74
|
+
"addition.base": "[加法・基底]",
|
|
75
|
+
"addition.recursive": "[加法・再帰]",
|
|
76
|
+
"multiplication.base": "[乗法・基底]",
|
|
77
|
+
"multiplication.recursive": "[乗法・再帰]",
|
|
78
|
+
"truncated": (
|
|
79
|
+
"…ログは{max_lines}行で省略しました。入力を小さくして再実行してください。"
|
|
80
|
+
),
|
|
81
|
+
"midpoint_root": "{polynomial}: 中点 {midpoint} は根",
|
|
82
|
+
},
|
|
83
|
+
"zh-Hans": {
|
|
84
|
+
"equality.zero": "[相等:零情形]",
|
|
85
|
+
"equality.successor": "[相等:后继情形]",
|
|
86
|
+
"addition.base": "[加法:基础情形]",
|
|
87
|
+
"addition.recursive": "[加法:递归情形]",
|
|
88
|
+
"multiplication.base": "[乘法:基础情形]",
|
|
89
|
+
"multiplication.recursive": "[乘法:递归情形]",
|
|
90
|
+
"truncated": "…日志在{max_lines}行后截断。请减小输入后重新运行。",
|
|
91
|
+
"midpoint_root": "{polynomial}:中点 {midpoint} 是根",
|
|
92
|
+
},
|
|
93
|
+
"zh-Hant": {
|
|
94
|
+
"equality.zero": "[相等:零情形]",
|
|
95
|
+
"equality.successor": "[相等:後繼情形]",
|
|
96
|
+
"addition.base": "[加法:基礎情形]",
|
|
97
|
+
"addition.recursive": "[加法:遞迴情形]",
|
|
98
|
+
"multiplication.base": "[乘法:基礎情形]",
|
|
99
|
+
"multiplication.recursive": "[乘法:遞迴情形]",
|
|
100
|
+
"truncated": "…記錄在{max_lines}行後截斷。請縮小輸入後重新執行。",
|
|
101
|
+
"midpoint_root": "{polynomial}:中點 {midpoint} 是根",
|
|
102
|
+
},
|
|
103
|
+
"es": {
|
|
104
|
+
"equality.zero": "[igualdad: caso cero]",
|
|
105
|
+
"equality.successor": "[igualdad: caso sucesor]",
|
|
106
|
+
"addition.base": "[suma: caso base]",
|
|
107
|
+
"addition.recursive": "[suma: caso recursivo]",
|
|
108
|
+
"multiplication.base": "[multiplicación: caso base]",
|
|
109
|
+
"multiplication.recursive": "[multiplicación: caso recursivo]",
|
|
110
|
+
"truncated": (
|
|
111
|
+
"…El registro se truncó tras {max_lines} líneas. "
|
|
112
|
+
"Reduce la entrada y vuelve a ejecutar."
|
|
113
|
+
),
|
|
114
|
+
"midpoint_root": "{polynomial}: el punto medio {midpoint} es una raíz",
|
|
115
|
+
},
|
|
116
|
+
"pt-BR": {
|
|
117
|
+
"equality.zero": "[igualdade: caso zero]",
|
|
118
|
+
"equality.successor": "[igualdade: caso sucessor]",
|
|
119
|
+
"addition.base": "[adição: caso base]",
|
|
120
|
+
"addition.recursive": "[adição: caso recursivo]",
|
|
121
|
+
"multiplication.base": "[multiplicação: caso base]",
|
|
122
|
+
"multiplication.recursive": "[multiplicação: caso recursivo]",
|
|
123
|
+
"truncated": (
|
|
124
|
+
"…O log foi truncado após {max_lines} linhas. "
|
|
125
|
+
"Reduza a entrada e execute novamente."
|
|
126
|
+
),
|
|
127
|
+
"midpoint_root": "{polynomial}: o ponto médio {midpoint} é uma raiz",
|
|
128
|
+
},
|
|
129
|
+
"fr": {
|
|
130
|
+
"equality.zero": "[égalité : cas zéro]",
|
|
131
|
+
"equality.successor": "[égalité : cas successeur]",
|
|
132
|
+
"addition.base": "[addition : cas de base]",
|
|
133
|
+
"addition.recursive": "[addition : cas récursif]",
|
|
134
|
+
"multiplication.base": "[multiplication : cas de base]",
|
|
135
|
+
"multiplication.recursive": "[multiplication : cas récursif]",
|
|
136
|
+
"truncated": (
|
|
137
|
+
"…Le journal a été tronqué après {max_lines} lignes. "
|
|
138
|
+
"Réduisez les entrées et relancez."
|
|
139
|
+
),
|
|
140
|
+
"midpoint_root": "{polynomial} : le milieu {midpoint} est une racine",
|
|
141
|
+
},
|
|
142
|
+
"de": {
|
|
143
|
+
"equality.zero": "[Gleichheit: Nullfall]",
|
|
144
|
+
"equality.successor": "[Gleichheit: Nachfolgerfall]",
|
|
145
|
+
"addition.base": "[Addition: Basisfall]",
|
|
146
|
+
"addition.recursive": "[Addition: Rekursionsfall]",
|
|
147
|
+
"multiplication.base": "[Multiplikation: Basisfall]",
|
|
148
|
+
"multiplication.recursive": "[Multiplikation: Rekursionsfall]",
|
|
149
|
+
"truncated": (
|
|
150
|
+
"…Die Protokollausgabe wurde nach {max_lines} Zeilen gekürzt. "
|
|
151
|
+
"Verkleinern Sie die Eingaben und führen Sie die Zelle erneut aus."
|
|
152
|
+
),
|
|
153
|
+
"midpoint_root": "{polynomial}: Der Mittelpunkt {midpoint} ist eine Nullstelle",
|
|
154
|
+
},
|
|
155
|
+
"ko": {
|
|
156
|
+
"equality.zero": "[같음: 0인 경우]",
|
|
157
|
+
"equality.successor": "[같음: 후속자 경우]",
|
|
158
|
+
"addition.base": "[덧셈: 기저 경우]",
|
|
159
|
+
"addition.recursive": "[덧셈: 재귀 경우]",
|
|
160
|
+
"multiplication.base": "[곱셈: 기저 경우]",
|
|
161
|
+
"multiplication.recursive": "[곱셈: 재귀 경우]",
|
|
162
|
+
"truncated": (
|
|
163
|
+
"…로그를 {max_lines}줄에서 줄였습니다. 입력을 작게 바꾸고 다시 실행하세요."
|
|
164
|
+
),
|
|
165
|
+
"midpoint_root": "{polynomial}: 중점 {midpoint}은(는) 근입니다",
|
|
166
|
+
},
|
|
167
|
+
"ru": {
|
|
168
|
+
"equality.zero": "[равенство: случай нуля]",
|
|
169
|
+
"equality.successor": "[равенство: случай следующего]",
|
|
170
|
+
"addition.base": "[сложение: базовый случай]",
|
|
171
|
+
"addition.recursive": "[сложение: рекурсивный случай]",
|
|
172
|
+
"multiplication.base": "[умножение: базовый случай]",
|
|
173
|
+
"multiplication.recursive": "[умножение: рекурсивный случай]",
|
|
174
|
+
"truncated": (
|
|
175
|
+
"…Журнал обрезан после {max_lines} строк. "
|
|
176
|
+
"Уменьшите входные данные и запустите ячейку снова."
|
|
177
|
+
),
|
|
178
|
+
"midpoint_root": "{polynomial}: середина {midpoint} является корнем",
|
|
179
|
+
},
|
|
180
|
+
"ar": {
|
|
181
|
+
"equality.zero": "[المساواة: حالة الصفر]",
|
|
182
|
+
"equality.successor": "[المساواة: حالة الخليفة]",
|
|
183
|
+
"addition.base": "[الجمع: الحالة الأساسية]",
|
|
184
|
+
"addition.recursive": "[الجمع: الحالة العودية]",
|
|
185
|
+
"multiplication.base": "[الضرب: الحالة الأساسية]",
|
|
186
|
+
"multiplication.recursive": "[الضرب: الحالة العودية]",
|
|
187
|
+
"truncated": (
|
|
188
|
+
"…اختُصر السجل بعد {max_lines} سطرًا. صغّر المدخلات ثم شغّل الخلية من جديد."
|
|
189
|
+
),
|
|
190
|
+
"midpoint_root": "{polynomial}: نقطة المنتصف {midpoint} جذر",
|
|
191
|
+
},
|
|
192
|
+
"hi": {
|
|
193
|
+
"equality.zero": "[समानता: शून्य स्थिति]",
|
|
194
|
+
"equality.successor": "[समानता: उत्तराधिकारी स्थिति]",
|
|
195
|
+
"addition.base": "[जोड़: आधार स्थिति]",
|
|
196
|
+
"addition.recursive": "[जोड़: पुनरावर्ती स्थिति]",
|
|
197
|
+
"multiplication.base": "[गुणा: आधार स्थिति]",
|
|
198
|
+
"multiplication.recursive": "[गुणा: पुनरावर्ती स्थिति]",
|
|
199
|
+
"truncated": (
|
|
200
|
+
"…लॉग {max_lines} पंक्तियों के बाद छोटा कर दिया गया। इनपुट घटाकर सेल फिर चलाएँ।"
|
|
201
|
+
),
|
|
202
|
+
"midpoint_root": "{polynomial}: मध्यबिंदु {midpoint} एक मूल है",
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def translate(key: str, **values: object) -> str:
|
|
208
|
+
"""Return a runtime message in the configured locale."""
|
|
209
|
+
|
|
210
|
+
return _MESSAGES[_locale][key].format(**values)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def localized(english: str, japanese: str) -> str:
|
|
214
|
+
"""Return an English/Japanese compatibility message.
|
|
215
|
+
|
|
216
|
+
New runtime messages should use :func:`translate`.
|
|
217
|
+
"""
|
|
218
|
+
return japanese if _locale == "ja" else english
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class _LineLimitFormatter(Formatter):
|
|
222
|
+
"""Format a copied record when emitting the truncation notice."""
|
|
223
|
+
|
|
224
|
+
def format(self, record: LogRecord) -> str:
|
|
225
|
+
max_lines = getattr(record, _LOG_LIMIT_NOTICE, None)
|
|
226
|
+
if not isinstance(max_lines, int):
|
|
227
|
+
return super().format(record)
|
|
228
|
+
notice = copy(record)
|
|
229
|
+
notice.msg = translate("truncated", max_lines=max_lines)
|
|
230
|
+
notice.args = ()
|
|
231
|
+
return super().format(notice)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class _LineLimitFilter(Filter):
|
|
235
|
+
"""Emit at most ``max_lines`` messages plus one truncation notice."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, max_lines: int | None) -> None:
|
|
238
|
+
super().__init__(__name__)
|
|
239
|
+
self.max_lines = max_lines
|
|
240
|
+
self.emitted_lines = 0
|
|
241
|
+
|
|
242
|
+
def filter(self, record: LogRecord) -> bool:
|
|
243
|
+
if not super().filter(record):
|
|
244
|
+
return False
|
|
245
|
+
if self.max_lines is None:
|
|
246
|
+
return True
|
|
247
|
+
if self.emitted_lines < self.max_lines:
|
|
248
|
+
self.emitted_lines += 1
|
|
249
|
+
return True
|
|
250
|
+
if self.emitted_lines == self.max_lines:
|
|
251
|
+
self.emitted_lines += 1
|
|
252
|
+
setattr(record, _LOG_LIMIT_NOTICE, self.max_lines)
|
|
253
|
+
return True
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def log(
|
|
258
|
+
log_level: int,
|
|
259
|
+
) -> Callable[[Callable[P, tuple[T, LogMessage]]], Callable[P, T]]:
|
|
260
|
+
"""Turn an operation returning a result and message into a public operation.
|
|
261
|
+
|
|
262
|
+
The implementation returns ``(result, formula)``. Callers receive only
|
|
263
|
+
``result`` while ``formula`` is logged at the requested level. Callable
|
|
264
|
+
messages are evaluated lazily only when logging is enabled.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
def outer(func: Callable[P, tuple[T, LogMessage]]) -> Callable[P, T]:
|
|
268
|
+
@wraps(func)
|
|
269
|
+
def inner(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
270
|
+
result, message = func(*args, **kwargs)
|
|
271
|
+
if logger.isEnabledFor(log_level):
|
|
272
|
+
logger.log(
|
|
273
|
+
log_level, message if isinstance(message, str) else message()
|
|
274
|
+
)
|
|
275
|
+
return result
|
|
276
|
+
|
|
277
|
+
public_return = _public_return_annotation(
|
|
278
|
+
signature(func, follow_wrapped=False).return_annotation
|
|
279
|
+
)
|
|
280
|
+
inner.__annotations__ = {
|
|
281
|
+
**func.__annotations__,
|
|
282
|
+
"return": public_return,
|
|
283
|
+
}
|
|
284
|
+
setattr(
|
|
285
|
+
inner,
|
|
286
|
+
"__signature__",
|
|
287
|
+
signature(func, follow_wrapped=False).replace(
|
|
288
|
+
return_annotation=public_return
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
return inner
|
|
292
|
+
|
|
293
|
+
return outer
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _public_return_annotation(annotation: object) -> object:
|
|
297
|
+
"""Extract the public result type from the internal tuple annotation."""
|
|
298
|
+
|
|
299
|
+
if isinstance(annotation, str):
|
|
300
|
+
prefix = "tuple["
|
|
301
|
+
for suffix in (", str]", ", LogMessage]"):
|
|
302
|
+
if annotation.startswith(prefix) and annotation.endswith(suffix):
|
|
303
|
+
return annotation[len(prefix) : -len(suffix)]
|
|
304
|
+
elif get_origin(annotation) is tuple:
|
|
305
|
+
arguments = get_args(annotation)
|
|
306
|
+
if len(arguments) == 2 and arguments[1] in (str, LogMessage):
|
|
307
|
+
return arguments[0]
|
|
308
|
+
raise TypeError("functions decorated with log must return (result, LogMessage)")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _remove_peano_handlers(*loggers: Logger) -> None:
|
|
312
|
+
"""Remove only handlers installed by this module."""
|
|
313
|
+
|
|
314
|
+
for target in loggers:
|
|
315
|
+
for handler in tuple(target.handlers):
|
|
316
|
+
if getattr(handler, _PEANO_HANDLER_MARKER, False):
|
|
317
|
+
target.removeHandler(handler)
|
|
318
|
+
handler.close()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def config_log(
|
|
322
|
+
log_level: int = 0,
|
|
323
|
+
root: bool = False,
|
|
324
|
+
fmt: str = "%(message)s",
|
|
325
|
+
clear_handlers: bool = True,
|
|
326
|
+
max_lines: int | None = None,
|
|
327
|
+
locale: Locale = "en",
|
|
328
|
+
) -> None:
|
|
329
|
+
"""Write symbolic evaluation logs to standard error.
|
|
330
|
+
|
|
331
|
+
By default, only the library logger is configured. ``root=True`` preserves
|
|
332
|
+
existing root handlers and manages only the handler installed here.
|
|
333
|
+
``max_lines`` emits one truncation notice after the limit. Pass
|
|
334
|
+
``"Level %(levelno)s: %(message)s"`` as ``fmt`` to expose internal levels.
|
|
335
|
+
Runtime labels are English by default. ``SUPPORTED_LOCALES`` lists the
|
|
336
|
+
available translations.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
global _locale
|
|
340
|
+
if max_lines is not None and (
|
|
341
|
+
isinstance(max_lines, bool) or not isinstance(max_lines, int) or max_lines < 1
|
|
342
|
+
):
|
|
343
|
+
raise ValueError("max_lines must be a positive integer or None")
|
|
344
|
+
if locale not in SUPPORTED_LOCALES:
|
|
345
|
+
supported = ", ".join(SUPPORTED_LOCALES)
|
|
346
|
+
raise ValueError(f"locale must be one of: {supported}")
|
|
347
|
+
_locale = locale
|
|
348
|
+
|
|
349
|
+
root_logger = getLogger()
|
|
350
|
+
if clear_handlers:
|
|
351
|
+
_remove_peano_handlers(logger, root_logger)
|
|
352
|
+
|
|
353
|
+
handler = StreamHandler()
|
|
354
|
+
handler.setFormatter(_LineLimitFormatter(fmt))
|
|
355
|
+
handler.setLevel(log_level)
|
|
356
|
+
handler.addFilter(_LineLimitFilter(max_lines))
|
|
357
|
+
setattr(handler, _PEANO_HANDLER_MARKER, True)
|
|
358
|
+
|
|
359
|
+
logger.setLevel(log_level)
|
|
360
|
+
logger.disabled = False
|
|
361
|
+
if root:
|
|
362
|
+
logger.propagate = True
|
|
363
|
+
root_logger.addHandler(handler)
|
|
364
|
+
else:
|
|
365
|
+
logger.propagate = False
|
|
366
|
+
logger.addHandler(handler)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def explain(value: object) -> str:
|
|
370
|
+
"""Render a value consistently inside symbolic log messages."""
|
|
371
|
+
|
|
372
|
+
return repr(value)
|
|
@@ -1,189 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|