mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Compare commits
1994 Commits
v0.2.0
...
2025-06-09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e094eaa55 | ||
|
|
2e368baf2a | ||
|
|
323ea3b96b | ||
|
|
e27d03179f | ||
|
|
81a9002ef2 | ||
|
|
18521290bf | ||
|
|
5e682e3f17 | ||
|
|
163296d306 | ||
|
|
1ae98ee177 | ||
|
|
2c02e2776b | ||
|
|
72dc4954ad | ||
|
|
b696e56c5f | ||
|
|
d11d5c65e6 | ||
|
|
5c0f70c361 | ||
|
|
4e2e0b41c6 | ||
|
|
df380bca96 | ||
|
|
9bd7f1810b | ||
|
|
31e6cca916 | ||
|
|
b8095b84ff | ||
|
|
908386091b | ||
|
|
aee4c7ac69 | ||
|
|
dc75d203ff | ||
|
|
ce5524d72a | ||
|
|
06c4b151d6 | ||
|
|
79646fd222 | ||
|
|
d9c18c5593 | ||
|
|
acae154f1b | ||
|
|
a5016446f4 | ||
|
|
2042d877f9 | ||
|
|
2a1ea45659 | ||
|
|
48b08a2b7f | ||
|
|
9c88475b31 | ||
|
|
6aa80aa596 | ||
|
|
85f7ba51f4 | ||
|
|
94b38a51c4 | ||
|
|
253cc4e846 | ||
|
|
431b900084 | ||
|
|
301c32dba0 | ||
|
|
cd1c9be0e1 | ||
|
|
6461a91933 | ||
|
|
e49e743669 | ||
|
|
e8df06582e | ||
|
|
02f120aaf4 | ||
|
|
b84d6a36a6 | ||
|
|
d73f03b9ba | ||
|
|
1a4b035dac | ||
|
|
dd40bf7566 | ||
|
|
5e770e9c9e | ||
|
|
d1b7dc551c | ||
|
|
b0991e28a2 | ||
|
|
180746467e | ||
|
|
f55bf8f83b | ||
|
|
ff10a64727 | ||
|
|
5561b6ead4 | ||
|
|
392d1c04f6 | ||
|
|
d46bcd9291 | ||
|
|
ca496fb3b1 | ||
|
|
7aad6e03e3 | ||
|
|
c97f4d1daf | ||
|
|
7d2a7a0e35 | ||
|
|
92e72aabdc | ||
|
|
8603cd9387 | ||
|
|
1c64bde0ee | ||
|
|
70f3aec552 | ||
|
|
6567d1d6ec | ||
|
|
a5214a0de7 | ||
|
|
a85a84330f | ||
|
|
494918d9fe | ||
|
|
3bfafb0ecb | ||
|
|
ecbc6f7044 | ||
|
|
5ce860443c | ||
|
|
320d74527f | ||
|
|
82a62382d0 | ||
|
|
fbaeecc6a1 | ||
|
|
e434ff5f6e | ||
|
|
f0d46bfeaa | ||
|
|
e377e43f05 | ||
|
|
e640487572 | ||
|
|
397a1968c8 | ||
|
|
783e45f8ac | ||
|
|
fc331a154f | ||
|
|
12ceb9695c | ||
|
|
974c54ede2 | ||
|
|
a4d1bba74e | ||
|
|
960954eba5 | ||
|
|
dabd93c255 | ||
|
|
0d4faa00a7 | ||
|
|
fd665f6bb2 | ||
|
|
ecdb7d3229 | ||
|
|
10fd02e42e | ||
|
|
c53908fe9e | ||
|
|
b72f3a4928 | ||
|
|
55998c9e3b | ||
|
|
e73b4e9734 | ||
|
|
2e14b7b5e8 | ||
|
|
7eb361c92f | ||
|
|
2ca52bf3ba | ||
|
|
2e260761ae | ||
|
|
99a384a3c7 | ||
|
|
662d3a1b4a | ||
|
|
3a1a5d3cd0 | ||
|
|
a0226df166 | ||
|
|
4336b9e787 | ||
|
|
844090b0b8 | ||
|
|
213506d9ae | ||
|
|
4d53f5925c | ||
|
|
21272025c2 | ||
|
|
d44324d4d0 | ||
|
|
628287c14e | ||
|
|
e949c9aa3f | ||
|
|
09c199a1ba | ||
|
|
d47944b2fd | ||
|
|
456e555e8b | ||
|
|
c7042fd847 | ||
|
|
a917da3b1a | ||
|
|
fb0c4b6b3c | ||
|
|
49b348cc7e | ||
|
|
a7ad848270 | ||
|
|
c2c20758c9 | ||
|
|
c7df344805 | ||
|
|
4094c5bfc9 | ||
|
|
4ae2936a45 | ||
|
|
fd2764c7c7 | ||
|
|
b81ae9b954 | ||
|
|
d96374faba | ||
|
|
02533ace81 | ||
|
|
22333e755b | ||
|
|
8dc1718002 | ||
|
|
ad5ffb648f | ||
|
|
861055f558 | ||
|
|
3c6bc2cf9f | ||
|
|
be56911598 | ||
|
|
98137eb79c | ||
|
|
2230d6c751 | ||
|
|
d800a6bb98 | ||
|
|
e0a35e4322 | ||
|
|
c2665e38ba | ||
|
|
ab4dffb53c | ||
|
|
36cce6b174 | ||
|
|
5c854fc690 | ||
|
|
883e0cab29 | ||
|
|
d7113e11db | ||
|
|
66cf905e8b | ||
|
|
7ac61f3840 | ||
|
|
2c94b809ae | ||
|
|
d52081fe41 | ||
|
|
e7f04612f6 | ||
|
|
fd4ad3e4d1 | ||
|
|
f1d45ee5a7 | ||
|
|
6620aa07af | ||
|
|
8063148598 | ||
|
|
2bf2332806 | ||
|
|
64a0721616 | ||
|
|
c3ed002b12 | ||
|
|
f6a9754b4e | ||
|
|
264f3d792a | ||
|
|
cb967c697b | ||
|
|
e21a04cf4b | ||
|
|
f0bcad7116 | ||
|
|
57a83db69d | ||
|
|
3ad8fd711f | ||
|
|
160363fa46 | ||
|
|
0b35946972 | ||
|
|
24d995678f | ||
|
|
8e7039405e | ||
|
|
8f989e4a67 | ||
|
|
696dceacdc | ||
|
|
9e2f6bd187 | ||
|
|
b620f03728 | ||
|
|
ade45c2312 | ||
|
|
b067986576 | ||
|
|
763ba9fd6a | ||
|
|
fd270775a3 | ||
|
|
b99e7f62b2 | ||
|
|
bb8606dbed | ||
|
|
0abd8b1d87 | ||
|
|
58a17f337d | ||
|
|
d3d92bbb6f | ||
|
|
8081e0d281 | ||
|
|
f398321b1f | ||
|
|
7d05f881c4 | ||
|
|
030243a6f9 | ||
|
|
6b72d2ef5d | ||
|
|
b6aacbf401 | ||
|
|
dd467f6c73 | ||
|
|
cd89aa51f0 | ||
|
|
f27c1f7ea3 | ||
|
|
c7ca173c90 | ||
|
|
c9161c02b6 | ||
|
|
6e79a2aa8a | ||
|
|
bea25a0285 | ||
|
|
c96fd3d900 | ||
|
|
0a07cd931f | ||
|
|
c6cab4c43a | ||
|
|
2ab8716c95 | ||
|
|
e3d96aa3ca | ||
|
|
10d2837041 | ||
|
|
372e683063 | ||
|
|
5f6f6cce92 | ||
|
|
27bcba3027 | ||
|
|
053583f5a0 | ||
|
|
5e0eace8d9 | ||
|
|
e7fdfca5f5 | ||
|
|
2d4eec88d3 | ||
|
|
7f94c10be7 | ||
|
|
549cce24c8 | ||
|
|
97fa11d526 | ||
|
|
ad5788589b | ||
|
|
ec09599d84 | ||
|
|
f323d14ed3 | ||
|
|
bc38e9dedd | ||
|
|
7ac90f5cbc | ||
|
|
f3b8d5515a | ||
|
|
bd55baefa6 | ||
|
|
a86126419c | ||
|
|
5c22697344 | ||
|
|
cc6f3d3051 | ||
|
|
b36b32bfe8 | ||
|
|
3945d3b2fe | ||
|
|
ba1b5811ee | ||
|
|
7f4582bb23 | ||
|
|
cace112b1a | ||
|
|
e3a1031081 | ||
|
|
2a41072b44 | ||
|
|
01d470ff77 | ||
|
|
9779de98b8 | ||
|
|
c585678ec9 | ||
|
|
eaf4cdf5e1 | ||
|
|
948368fdb4 | ||
| 0717d5a331 | |||
| a9bfaa96c5 | |||
| 70a5774737 | |||
| 2d3b125d51 | |||
| 4081c08b5a | |||
| 70c36a48a8 | |||
|
|
37bd49cf38 | ||
|
|
081dc4e8ca | ||
|
|
a4466adf8b | ||
|
|
bfe72689fc | ||
|
|
950a8d5694 | ||
|
|
a6b4ef7f5d | ||
|
|
45c0fa0e77 | ||
|
|
a596568151 | ||
|
|
bd94d8d50c | ||
|
|
7fab64ed9c | ||
|
|
8e22c399df | ||
|
|
7546ea91a9 | ||
|
|
8da66978bf | ||
|
|
8484bfa2e0 | ||
|
|
ff970b0e1c | ||
|
|
8be7e4327d | ||
|
|
82eeb237dc | ||
|
|
cbbadf562f | ||
|
|
4308321f39 | ||
|
|
985eebf9b0 | ||
|
|
bf28152a32 | ||
|
|
bae0ad3aeb | ||
|
|
87fae150da | ||
|
|
97853bf0f1 | ||
|
|
cc0a1ce9e2 | ||
|
|
58ebf04bac | ||
|
|
7fea1e1b4a | ||
|
|
d2bf31724f | ||
|
|
b4929d258d | ||
|
|
ddf2e591c6 | ||
|
|
33940726a8 | ||
|
|
05cb8c0b73 | ||
|
|
8d2c6807d2 | ||
|
|
4d9804f188 | ||
|
|
3ae1160868 | ||
|
|
6804dd4363 | ||
|
|
defcadafbb | ||
|
|
40e3f49ab7 | ||
|
|
56196890f5 | ||
|
|
6daee1b00e | ||
|
|
8ff856d7ce | ||
|
|
6731c4b1ab | ||
|
|
544182ebfc | ||
|
|
12c3fa0b87 | ||
|
|
aa4774fe32 | ||
|
|
26bc4ba3dd | ||
|
|
b2abb1af84 | ||
|
|
cebbca9e63 | ||
|
|
1a2dda502a | ||
|
|
b870b0e1b5 | ||
|
|
df2354fdb7 | ||
|
|
c20c90fbc4 | ||
|
|
aba3d5c082 | ||
|
|
8c5602f2fb | ||
|
|
4468dcbe34 | ||
|
|
235adafa0b | ||
|
|
864e8598f8 | ||
|
|
f426348a94 | ||
|
|
085ac88438 | ||
|
|
4881f61be6 | ||
|
|
ff9947ff14 | ||
|
|
92e02a7f79 | ||
|
|
0a8b0406f5 | ||
|
|
1c3b198a17 | ||
|
|
52208b3c90 | ||
|
|
2721f2de3f | ||
|
|
b55a55afc7 | ||
|
|
d7a72b5755 | ||
|
|
1f3a9672c3 | ||
|
|
31c5c3eb9d | ||
|
|
7fada8b97e | ||
|
|
429754fd33 | ||
|
|
b4f0a589ed | ||
|
|
331a3c108f | ||
|
|
d698b28ce5 | ||
|
|
23236aa8c7 | ||
|
|
a9331bb34d | ||
|
|
65dcf1ce1c | ||
|
|
e2b0fe4266 | ||
|
|
fa2acd7cde | ||
|
|
a71c16f8cb | ||
|
|
f466971312 | ||
|
|
69b1a9910f | ||
|
|
4ed735b424 | ||
|
|
175afd97d8 | ||
|
|
72338d578b | ||
|
|
9856d94f2d | ||
|
|
517ffed401 | ||
|
|
38a6a8d984 | ||
|
|
630c1ff8d1 | ||
|
|
7e1568a1ff | ||
|
|
6788010f7d | ||
|
|
9e310934d3 | ||
|
|
e8a3406624 | ||
|
|
fde87a340c | ||
|
|
19050afc3f | ||
|
|
e96557b3e1 | ||
|
|
a5364973d9 | ||
|
|
a46ce8ec3a | ||
|
|
6e35e20e49 | ||
|
|
2d5e4d89b0 | ||
|
|
c9e62002ec | ||
|
|
465627f104 | ||
|
|
3de1c2ab56 | ||
|
|
8f5cc6174c | ||
|
|
29d014a0e1 | ||
|
|
396a0ca563 | ||
|
|
a500178b3c | ||
|
|
7d770f55fb | ||
|
|
db283a66e8 | ||
|
|
c642aef8ca | ||
|
|
396df1a506 | ||
|
|
9c9fa7e537 | ||
|
|
86e2eb0648 | ||
|
|
491db2f0c6 | ||
|
|
f0fb375028 | ||
|
|
16d8bab61a | ||
|
|
2d83a67bd6 | ||
|
|
5ad7e97e05 | ||
|
|
b7a7b6b923 | ||
|
|
0e00d2328d | ||
|
|
53db70e784 | ||
|
|
76c699b4ba | ||
|
|
c901bc07a4 | ||
|
|
b7db23bbae | ||
|
|
389b20d977 | ||
|
|
d06459fa49 | ||
|
|
e2a55cbf34 | ||
|
|
a5e6ade9cb | ||
|
|
a1e32566d3 | ||
|
|
8c7bfb3e1a | ||
|
|
bb0480e978 | ||
|
|
2ccc745513 | ||
|
|
bea83fe94d | ||
|
|
3feaf689d8 | ||
|
|
bd627b58af | ||
|
|
c561d33cb2 | ||
|
|
c8fd3bd683 | ||
|
|
fef1e31634 | ||
|
|
1abaf87abe | ||
|
|
38593fbd85 | ||
|
|
8d187fd275 | ||
|
|
646cc81656 | ||
|
|
01f7536b36 | ||
|
|
97e5ec02f8 | ||
|
|
3dced01af0 | ||
| 0cf4534c5c | |||
| 044f66fba3 | |||
| 40a9ddad4e | |||
|
|
848db340da | ||
|
|
c6da4ffcdd | ||
|
|
8ac7e34be2 | ||
|
|
e4be882994 | ||
|
|
adc05e663f | ||
|
|
0bc236ac98 | ||
| da3d3d9296 | |||
| 6a6af6a952 | |||
| 582a4ab267 | |||
| ebde8546c9 | |||
| fffd0f5465 | |||
|
|
3b6db8e21a | ||
|
|
19a0b7dd76 | ||
|
|
9647ebe206 | ||
|
|
0c726a1275 | ||
|
|
f71eb55f1f | ||
|
|
edcc0b7dbc | ||
|
|
4910b308ee | ||
|
|
0cc1c323ed | ||
|
|
24fd19b35d | ||
|
|
fbd0c7a99e | ||
| 4f5b26698c | |||
| d887e5c24c | |||
| 0c69391bbd | |||
| 91598f9121 | |||
|
|
98d09e7816 | ||
| 7ae9432524 | |||
|
|
c883f0ad8a | ||
|
|
eae60113af | ||
|
|
1aab5240cf | ||
|
|
edb7abde5a | ||
|
|
29d95340b0 | ||
|
|
6fb19ac74f | ||
|
|
37dc28a69d | ||
|
|
7623668256 | ||
|
|
bbf7aacd4d | ||
|
|
d6c1e08ef4 | ||
|
|
d1f95f04a7 | ||
|
|
0dacf8a326 | ||
|
|
e534b10722 | ||
|
|
0785cc5aa9 | ||
|
|
48025e0102 | ||
|
|
eeb719e8f7 | ||
|
|
7933edad43 | ||
|
|
5f75728ede | ||
|
|
8cff0ed6c2 | ||
|
|
a8964f4108 | ||
|
|
740aeedca3 | ||
|
|
8152e7e62c | ||
|
|
b36c95b91e | ||
|
|
b5c1fd95dc | ||
|
|
23f7cbf1c3 | ||
|
|
ae78ecc2c5 | ||
|
|
dd06516d1c | ||
|
|
8066f36a4e | ||
|
|
3bbf6b9d53 | ||
|
|
8bc5944178 | ||
|
|
a13b99642b | ||
|
|
060db5983c | ||
|
|
42bba6920e | ||
|
|
ea11d78995 | ||
|
|
fe63ca762f | ||
|
|
a82982725e | ||
|
|
7dfb760421 | ||
|
|
b6e9a3f37e | ||
|
|
3f28309b7b | ||
|
|
d2a4a330f9 | ||
|
|
d8c35770ab | ||
|
|
dbb6794a41 | ||
|
|
63c9909aa0 | ||
|
|
f1dac5087e | ||
|
|
4f80d7013e | ||
|
|
2919df1df5 | ||
|
|
a2df2f014b | ||
|
|
8673169ee7 | ||
|
|
2c2e0fb172 | ||
|
|
d45c5de906 | ||
|
|
eb985beed6 | ||
|
|
ff9aa0fc54 | ||
|
|
0bd1a3efb2 | ||
|
|
08e7ec948b | ||
|
|
6092c07eb5 | ||
|
|
09d74336fa | ||
|
|
694354e5e6 | ||
|
|
55f04db6b8 | ||
|
|
6f8360a878 | ||
|
|
6ca4685fee | ||
|
|
d86b592add | ||
|
|
9944b3dac1 | ||
|
|
8ed79bd1af | ||
|
|
0600ae6213 | ||
|
|
623415d843 | ||
|
|
87b84a83cc | ||
|
|
aa5eba9723 | ||
|
|
c2cd883f93 | ||
|
|
43e20a8cd9 | ||
|
|
48299cdd7f | ||
|
|
2335ca0f72 | ||
|
|
e42c1a33b5 | ||
|
|
b1a38c2d50 | ||
|
|
f1f05303db | ||
|
|
8424a733a2 | ||
|
|
2bf7a4a08c | ||
|
|
b4bae8173b | ||
|
|
3c10888e3a | ||
|
|
1bd143027a | ||
|
|
18d31f2d5b | ||
|
|
e142d655b9 | ||
|
|
e1ce1f66cd | ||
|
|
08c9a4d86b | ||
|
|
a620b38ba0 | ||
|
|
8e3c0cd658 | ||
|
|
f709a2805d | ||
|
|
adbadfc4f5 | ||
|
|
9c7a9cbace | ||
|
|
1333688a4e | ||
|
|
17852b78d5 | ||
|
|
dd15ae5560 | ||
|
|
9d33990199 | ||
|
|
866e7cf371 | ||
|
|
f5c1596ddf | ||
|
|
64cff172a1 | ||
|
|
c10b99b29a | ||
|
|
bca90f191c | ||
|
|
7996a10116 | ||
|
|
db4562f67d | ||
|
|
3fd0382a51 | ||
|
|
3e98e5e86c | ||
|
|
7b17965b26 | ||
|
|
5c050d5779 | ||
|
|
5d6d1a6da7 | ||
|
|
b59d876e76 | ||
|
|
9028aede6e | ||
|
|
aa0353a501 | ||
|
|
63c3f31c99 | ||
|
|
2fe44af8ba | ||
|
|
515f0bf9c2 | ||
|
|
27a52a7962 | ||
|
|
61feb43aba | ||
|
|
07fdcb6160 | ||
|
|
ac08f4447f | ||
|
|
d243c90d0a | ||
|
|
72033ab689 | ||
|
|
3f63501fa8 | ||
|
|
019496e3a8 | ||
|
|
52695a9ece | ||
|
|
a20ce951e7 | ||
|
|
85fa157d5a | ||
|
|
1068219c56 | ||
|
|
427ec50624 | ||
|
|
52ce1509a5 | ||
|
|
fe06583643 | ||
|
|
3e3c69b5bc | ||
|
|
3b0802535d | ||
|
|
75415090bd | ||
|
|
6f664cb05a | ||
|
|
a8ea67178d | ||
|
|
64a464aefb | ||
|
|
67885ad9fa | ||
|
|
e5bf72e897 | ||
|
|
1f62190eef | ||
|
|
f839e6cc79 | ||
|
|
52aad1ec08 | ||
|
|
5f0d1252b6 | ||
|
|
3370f0f23d | ||
|
|
e1574b1485 | ||
|
|
78fae736f9 | ||
|
|
ed3811a65c | ||
|
|
4e915de35d | ||
|
|
94e6648ee5 | ||
|
|
75a985e63e | ||
|
|
5714558785 | ||
|
|
c3ccb5b7bf | ||
|
|
87849cda4f | ||
|
|
f62114c7eb | ||
|
|
24f57dde2f | ||
|
|
cdfcd8a4c9 | ||
|
|
1bc1370701 | ||
|
|
6b7b080827 | ||
|
|
3556e1320d | ||
|
|
621640ca71 | ||
|
|
2c0e439d0d | ||
|
|
a392d8425e | ||
|
|
0273d78de9 | ||
|
|
ed51d8dcf6 | ||
|
|
3d78ca8b09 | ||
|
|
1cec856c63 | ||
|
|
6212c81ec6 | ||
|
|
408459b883 | ||
|
|
f6d88042b5 | ||
|
|
b58bdc9e32 | ||
|
|
f3501f44cb | ||
|
|
61f37b10e2 | ||
|
|
2856aff757 | ||
|
|
3949ecc054 | ||
|
|
acd9ea57d7 | ||
|
|
794a2a1892 | ||
|
|
af8bed6d3f | ||
|
|
462f54f906 | ||
|
|
1c4e99cf2c | ||
|
|
a349b9bdfe | ||
|
|
75e790836a | ||
|
|
5ba677cd36 | ||
|
|
a7b761a89c | ||
|
|
6e0b00d60c | ||
|
|
c107a938be | ||
|
|
c6fc9cee8e | ||
|
|
99a4bf5eb5 | ||
|
|
46410ff933 | ||
|
|
75e868e71e | ||
|
|
53b1b4d682 | ||
|
|
566d9bee5c | ||
|
|
b8bf65d68e | ||
|
|
075c69a3cd | ||
|
|
ada10067dd | ||
|
|
d0f680b379 | ||
|
|
ca2c1d0b48 | ||
|
|
5fd5939395 | ||
|
|
e5ca631b52 | ||
|
|
3313fdeb32 | ||
|
|
2bd8ff0b6c | ||
|
|
5c9d6d455d | ||
|
|
8f6cf6fef7 | ||
|
|
a5f8d42d34 | ||
|
|
153ec2823d | ||
|
|
9f65a30504 | ||
|
|
b018123f19 | ||
|
|
97e3e969e4 | ||
|
|
84099514e6 | ||
|
|
3286e683e6 | ||
|
|
be6ea2a7b8 | ||
|
|
4a939141f6 | ||
|
|
1034477f1e | ||
|
|
49cfcd817d | ||
|
|
39822d7e57 | ||
|
|
142d333c5c | ||
|
|
13c491712b | ||
|
|
5c527c2a28 | ||
|
|
41979f0823 | ||
|
|
192b0a8fb5 | ||
|
|
6a7be1e142 | ||
|
|
b3666060d4 | ||
|
|
c9c07a6bbc | ||
|
|
d57287b89b | ||
|
|
79e7015a32 | ||
|
|
6c4ee951e0 | ||
|
|
f0f4633346 | ||
|
|
6c37e8f4e7 | ||
|
|
4d05416ce3 | ||
|
|
60993b9d23 | ||
|
|
959e7c11ce | ||
|
|
21ae739eec | ||
|
|
b1c3c9a9d6 | ||
|
|
bf985c8ac6 | ||
|
|
21c5eae717 | ||
|
|
4d5cf249a0 | ||
|
|
8c7b811135 | ||
|
|
940b879950 | ||
|
|
ccf650d4ca | ||
|
|
6342f16eb5 | ||
|
|
8816cd41d5 | ||
|
|
4b190eb412 | ||
|
|
9f24841f6d | ||
|
|
a0b6c36928 | ||
|
|
3f691de7a3 | ||
|
|
247572863a | ||
|
|
5a5ac35f36 | ||
|
|
220594f263 | ||
|
|
f541ca9dda | ||
|
|
6a64af1066 | ||
|
|
35141266c1 | ||
|
|
6adb72727e | ||
|
|
6eed8f42c7 | ||
|
|
ae72316629 | ||
|
|
90ab0e33a2 | ||
|
|
9f7eb08fca | ||
|
|
6a53ec3c5d | ||
|
|
c5550cd466 | ||
|
|
f6569313d7 | ||
|
|
880036c0ae | ||
|
|
da53eeea57 | ||
|
|
e349b0f18b | ||
|
|
34ffa93945 | ||
|
|
7be6308248 | ||
|
|
57e7f4db95 | ||
|
|
f5fbb5b06f | ||
|
|
d9375b9fe1 | ||
|
|
1e3d57817c | ||
|
|
ebe555203a | ||
|
|
280337a305 | ||
|
|
02cec859e5 | ||
|
|
1dd9a2fbe4 | ||
|
|
df363c0ba7 | ||
|
|
90724b32ec | ||
|
|
0a24e106ba | ||
|
|
e6c73883ea | ||
|
|
e315077630 | ||
|
|
85c427b842 | ||
|
|
5ee5531f32 | ||
|
|
3737f2a091 | ||
|
|
ac78517044 | ||
|
|
12601d0b44 | ||
|
|
b3a606d9df | ||
|
|
426e582ba0 | ||
|
|
92c8b371ae | ||
|
|
d8f2bd04ac | ||
|
|
855fa1411f | ||
|
|
4e7b3bc8f2 | ||
|
|
83d1ad8a2c | ||
|
|
7f02324dce | ||
|
|
d453026d19 | ||
|
|
2fde8e91e5 | ||
|
|
23ebbd021b | ||
|
|
9b974bda0d | ||
|
|
de7e4e49da | ||
|
|
ead42beff6 | ||
|
|
35229721ea | ||
|
|
2f8e5189d3 | ||
|
|
4c8cd67db9 | ||
|
|
54247df801 | ||
|
|
e4be47a08b | ||
|
|
d2bf69e354 | ||
|
|
88ee64d585 | ||
|
|
d0c827ed38 | ||
|
|
d26766b7bc | ||
|
|
e6c6f966f7 | ||
|
|
407f251866 | ||
|
|
6cd9e54427 | ||
|
|
c9ec4507ad | ||
|
|
9481df23e1 | ||
|
|
0bd8c2504c | ||
|
|
bcb35919a4 | ||
|
|
5dc6d5582a | ||
|
|
a8ab7dd388 | ||
|
|
7c722c23a5 | ||
|
|
83b8c3a3fc | ||
|
|
36a36b200d | ||
|
|
d297e73a55 | ||
|
|
572053df82 | ||
|
|
def5661728 | ||
|
|
078cd0d88c | ||
|
|
d061837467 | ||
|
|
defc3ac7f1 | ||
|
|
97a0705d2e | ||
|
|
a88c2fe000 | ||
|
|
2a62ef7344 | ||
|
|
258342e1ca | ||
|
|
69e8e4be43 | ||
|
|
1c93c1630b | ||
|
|
ec9f2cedd1 | ||
|
|
10055772d8 | ||
|
|
61e40de32b | ||
|
|
074d228a7a | ||
|
|
bf461cdebc | ||
|
|
ffc52ef87c | ||
|
|
ea1f72e92d | ||
|
|
9693ad9b11 | ||
|
|
bdf228eb42 | ||
|
|
c4b1cc69be | ||
|
|
36b9219e32 | ||
|
|
43d7c71a68 | ||
|
|
3eda1cf3b4 | ||
|
|
6917b4c2ca | ||
|
|
21fc2059b7 | ||
|
|
10e51ba689 | ||
|
|
c93ea30b3b | ||
|
|
118a00c012 | ||
|
|
f9b2d10c71 | ||
|
|
2fe129252f | ||
|
|
17e1152de6 | ||
|
|
454aa4b654 | ||
|
|
169368b7f0 | ||
|
|
41bdcfe221 | ||
|
|
003c45dbff | ||
|
|
9378497346 | ||
|
|
99ed744c57 | ||
|
|
a9ed7de28d | ||
|
|
39c0106e87 | ||
|
|
9070e12e0d | ||
|
|
d73cc5f58c | ||
|
|
a777d22a53 | ||
|
|
aaae566231 | ||
|
|
28f0fa48a4 | ||
|
|
1983138c91 | ||
|
|
9cc571be95 | ||
|
|
602015fca1 | ||
|
|
1ab133dae8 | ||
|
|
317f449454 | ||
|
|
32f662ae80 | ||
|
|
7236109d75 | ||
|
|
506c8a633e | ||
|
|
a309cb5d2c | ||
|
|
5001b2e572 | ||
|
|
756088e7fb | ||
|
|
4729ca3af0 | ||
|
|
cccfb08835 | ||
|
|
d01909a524 | ||
|
|
ee128eac7c | ||
|
|
6df9732965 | ||
|
|
8a84a479f1 | ||
|
|
7513017e21 | ||
|
|
adf0788895 | ||
|
|
fc91cd8bc7 | ||
|
|
459cad8407 | ||
|
|
863a5b5a49 | ||
|
|
1c6336b350 | ||
|
|
fb12a4c219 | ||
|
|
16a3edd432 | ||
|
|
ebc8f60e21 | ||
|
|
727f97fd48 | ||
|
|
de626b8627 | ||
|
|
f519ffdb18 | ||
|
|
57f9478d16 | ||
|
|
5700fa3953 | ||
|
|
44438bffbd | ||
|
|
dd0c393b45 | ||
|
|
a00a387735 | ||
|
|
b4ee044fa6 | ||
|
|
df98264dd0 | ||
|
|
b6c2179893 | ||
|
|
b5176fdbc0 | ||
|
|
79231ee399 | ||
|
|
c0f6a2f8c3 | ||
|
|
99531514c8 | ||
|
|
0fab6e6063 | ||
|
|
784b5f1b1c | ||
|
|
3945e9a4ed | ||
|
|
ad3f0aecaf | ||
|
|
1d76b762c7 | ||
|
|
f7c7398ba8 | ||
|
|
186eac5095 | ||
|
|
43ede611e3 | ||
|
|
d5e4af7a4b | ||
|
|
f1991c25bc | ||
|
|
c4e2d6e379 | ||
|
|
b11d554c11 | ||
|
|
b05d5920ab | ||
|
|
926c4cef27 | ||
|
|
5c6b4cbd3c | ||
|
|
b93199f007 | ||
|
|
07f013dae2 | ||
|
|
fa3eae677d | ||
|
|
c5364ca157 | ||
|
|
c28cca97d1 | ||
|
|
9a61716f74 | ||
|
|
700f2b9c12 | ||
|
|
999dcbdd1b | ||
|
|
06bb68a6c6 | ||
|
|
dc4f6994fb | ||
|
|
8fc2c39d62 | ||
|
|
4d6a180638 | ||
|
|
460e9833f0 | ||
|
|
c8256c5450 | ||
|
|
e06f5ccfe4 | ||
|
|
68dc5ca052 | ||
|
|
ac21576ab9 | ||
|
|
b0ebe58636 | ||
|
|
7833fd9b12 | ||
|
|
81208637f5 | ||
|
|
5303b33c8b | ||
|
|
f54b80f88c | ||
|
|
068249196f | ||
|
|
87cf891e50 | ||
|
|
6d23daa480 | ||
|
|
ee9f6ae079 | ||
|
|
488bac7413 | ||
|
|
8ff0872d41 | ||
|
|
a96926cd77 | ||
|
|
06b9b4938d | ||
|
|
b5e21ab136 | ||
|
|
fe617431f4 | ||
|
|
99d992f708 | ||
|
|
f682d184fb | ||
|
|
2bfbfd7a03 | ||
|
|
c32369bd27 | ||
|
|
b8f22e2967 | ||
|
|
b4b71e5a11 | ||
|
|
4dacbc51e2 | ||
|
|
af884cb284 | ||
|
|
a75f26b922 | ||
|
|
d32cb7efdd | ||
|
|
03576615e4 | ||
|
|
bda7c5cf06 | ||
|
|
0e72ba8819 | ||
|
|
4e6172b99d | ||
|
|
9241e2e5d5 | ||
|
|
830389f62c | ||
|
|
2c3dbb5eed | ||
|
|
f1b36233b6 | ||
|
|
eb83b729b2 | ||
|
|
b69e6a910d | ||
|
|
b9ee0b6b7a | ||
|
|
f8365ca6c3 | ||
|
|
c2f159b24a | ||
|
|
54d5869457 | ||
|
|
285ba765a7 | ||
|
|
91c0c8b002 | ||
|
|
4a61eba58e | ||
|
|
58df09b492 | ||
|
|
6313e4c9fb | ||
|
|
0f3a9311e0 | ||
|
|
987d50c092 | ||
|
|
2fa88f94b6 | ||
|
|
7022512b83 | ||
|
|
4135da42ac | ||
|
|
d975c51b96 | ||
|
|
23bf5c42ca | ||
|
|
c3a8d5a9b5 | ||
|
|
0d0139b322 | ||
|
|
5384a4766a | ||
|
|
a12233e788 | ||
|
|
80e7186aa9 | ||
|
|
9ea4baa41c | ||
|
|
0a76a9b115 | ||
|
|
fd98ab2084 | ||
|
|
48d4c22362 | ||
|
|
10e4f715a5 | ||
|
|
9031a0ac9f | ||
|
|
37ce45fffb | ||
|
|
39169de63a | ||
|
|
21cff29c31 | ||
|
|
589b3eb7c3 | ||
|
|
1208416b92 | ||
|
|
e5cea3ad37 | ||
|
|
8c11992e59 | ||
|
|
b864e5da1f | ||
|
|
77939d2ca5 | ||
|
|
9cf18a8bdc | ||
|
|
c25aa1add4 | ||
|
|
3900a086b8 | ||
|
|
64c66e00d6 | ||
|
|
4ba2892168 | ||
|
|
aee68d20bb | ||
|
|
d4be55c2ea | ||
|
|
e2f7d5b2f9 | ||
|
|
9417eec81e | ||
|
|
d056201e5b | ||
|
|
93d731cc4b | ||
|
|
d24013d582 | ||
|
|
31f0d9b282 | ||
|
|
f7d4413c62 | ||
|
|
3131d56298 | ||
|
|
9c2355117c | ||
|
|
94029386ae | ||
|
|
0fbe57f96b | ||
|
|
5f6059ef73 | ||
|
|
ba8d7b541f | ||
|
|
b1238ab4eb | ||
|
|
287f89aa04 | ||
|
|
d5a62848dd | ||
|
|
03f954408a | ||
|
|
d4d362a9ca | ||
|
|
aea4d509c9 | ||
|
|
bdb0c8f645 | ||
|
|
c28cb3941f | ||
|
|
d66f4d5315 | ||
|
|
b088787f7b | ||
|
|
6363940d6c | ||
|
|
a58ede99f2 | ||
|
|
7e66db0d43 | ||
|
|
179de9fb67 | ||
|
|
500b0024f0 | ||
|
|
4bec66e1c6 | ||
|
|
1cdc5d3294 | ||
|
|
3d2c51962b | ||
|
|
5c81649b19 | ||
|
|
d64d33c809 | ||
|
|
b875057ae2 | ||
|
|
2e5b0330c7 | ||
|
|
c7d3358582 | ||
|
|
1df0a44958 | ||
|
|
3c8fb93ab4 | ||
|
|
95ef76f7a0 | ||
|
|
9953597bb6 | ||
|
|
91e206faa1 | ||
|
|
06ed5cc8cf | ||
|
|
483dfcdd2c | ||
|
|
a4330c3558 | ||
|
|
a6a92128f0 | ||
|
|
436be2b96a | ||
|
|
89323dadc2 | ||
|
|
7e3183ef16 | ||
|
|
854d3e4d80 | ||
|
|
3150b4ded6 | ||
|
|
1be262db40 | ||
|
|
3d76e1e1db | ||
|
|
8af4c5fd7b | ||
|
|
8dbf7b27ef | ||
|
|
2b4f607cbc | ||
|
|
cbaed46fb4 | ||
|
|
9cab670bf8 | ||
|
|
6fb15535ed | ||
|
|
4721b7df92 | ||
|
|
06c6244599 | ||
|
|
3a2a1d1b74 | ||
|
|
531f4babff | ||
|
|
0f2ea6a1f3 | ||
|
|
7069a1de0a | ||
|
|
6f8f42b06a | ||
|
|
8db7edb6d9 | ||
|
|
de0d323ad3 | ||
|
|
01486830e3 | ||
|
|
7a3f965396 | ||
|
|
058f8c5500 | ||
|
|
0e24cf48c6 | ||
|
|
e864e26592 | ||
|
|
59df92d700 | ||
|
|
283a8046aa | ||
|
|
9ba6027599 | ||
|
|
8312831bc9 | ||
|
|
2786831d32 | ||
|
|
7366a41b1c | ||
|
|
2b43d4817c | ||
|
|
050f0bd0ad | ||
|
|
7c56313d2b | ||
|
|
ab3862d7f4 | ||
|
|
f4b54082cf | ||
|
|
ff7055749c | ||
|
|
a1d9c649f6 | ||
|
|
1aeaf4afef | ||
|
|
5d3e12fca4 | ||
|
|
3ae6ce5216 | ||
|
|
f5fc30ae63 | ||
|
|
7a6000d181 | ||
|
|
15fdf1ee5b | ||
|
|
4c2ab63185 | ||
|
|
0a10e6cbea | ||
|
|
935e35bb40 | ||
|
|
698134caab | ||
|
|
f12875027c | ||
|
|
833371918d | ||
|
|
a5f08d77a7 | ||
|
|
f3e4d34092 | ||
|
|
a43a8c546e | ||
|
|
b85e134477 | ||
|
|
4064c84cb4 | ||
|
|
dc569e9921 | ||
|
|
5337dedadf | ||
|
|
f1b261b4f8 | ||
|
|
a5ed4070bf | ||
|
|
fa30d33c9b | ||
|
|
3d9fb36cde | ||
|
|
6603c46b41 | ||
|
|
c7025f70d5 | ||
|
|
d03c7267d6 | ||
|
|
0af106c85c | ||
|
|
da15bb282e | ||
|
|
ff808ad313 | ||
|
|
0897385a30 | ||
|
|
bac1cdc55f | ||
|
|
91b9ed17c2 | ||
|
|
f2cfa5f0a7 | ||
|
|
306943a9f5 | ||
|
|
ff5076b12c | ||
|
|
cea23d2b07 | ||
|
|
1dd5e84c9a | ||
|
|
cfbdf7f924 | ||
|
|
2080cc2067 | ||
|
|
9cc8f2dfa9 | ||
|
|
2315ce956e | ||
|
|
e73603945d | ||
|
|
98eef0804e | ||
|
|
fa79055821 | ||
|
|
4a099ab6bc | ||
|
|
9f58921a6f | ||
|
|
9b9dd1100a | ||
|
|
2abf21b134 | ||
|
|
f4b9bdfb29 | ||
|
|
429d1d1cce | ||
|
|
9d44f93192 | ||
|
|
b8bca5a148 | ||
|
|
8c1d923f3e | ||
|
|
04749c13f4 | ||
|
|
e2ccb48463 | ||
|
|
02840593bc | ||
|
|
8503e0de90 | ||
|
|
23fee2737c | ||
|
|
0a94e1393a | ||
|
|
9287169201 | ||
|
|
8605fea678 | ||
|
|
d9b6585804 | ||
|
|
30b8d7bdad | ||
|
|
436624b7a3 | ||
|
|
268a39fc61 | ||
|
|
317f432a20 | ||
|
|
cbcdcf4ad5 | ||
|
|
ce13fc5f03 | ||
|
|
c16f813794 | ||
|
|
6e9e15b929 | ||
|
|
776bda60ca | ||
|
|
322aa6887a | ||
|
|
f256934f93 | ||
|
|
2658c5c539 | ||
|
|
4e0890a50d | ||
|
|
73e1eeb57e | ||
|
|
514014f4fc | ||
|
|
508cf6b61d | ||
|
|
8782bf8cb0 | ||
|
|
94bdb6b97a | ||
|
|
2c90b128c3 | ||
|
|
cf465bdd81 | ||
|
|
05442bc667 | ||
|
|
aad1d848a6 | ||
|
|
09a1ce9b2b | ||
|
|
d9c808ded8 | ||
|
|
ba6757ad11 | ||
|
|
e18dda3e08 | ||
|
|
1fa69ebcc9 | ||
|
|
f0db8329be | ||
|
|
6ce372cfb6 | ||
|
|
7fc30b715a | ||
|
|
07c07ad806 | ||
|
|
d7e7001afd | ||
|
|
df6831694b | ||
|
|
14aa9d7108 | ||
|
|
3794c178be | ||
|
|
560cd6ca6c | ||
|
|
6996141448 | ||
|
|
968f2ad7c9 | ||
|
|
c05f23d99d | ||
|
|
19224505e8 | ||
|
|
06b0484ea3 | ||
|
|
9e4c8ad7d7 | ||
|
|
e103adaa52 | ||
|
|
89e3587c15 | ||
|
|
446285f348 | ||
|
|
c03d3a58ec | ||
|
|
334c15e879 | ||
|
|
c3d9a51d15 | ||
|
|
fddbf0728a | ||
|
|
32aa9933be | ||
|
|
a354f7bba1 | ||
|
|
b7811af211 | ||
|
|
471ec26873 | ||
|
|
a4a6e8dabe | ||
|
|
4416b2a467 | ||
|
|
c69ff172e5 | ||
|
|
9c38e904dd | ||
|
|
fd4f6941bf | ||
|
|
9f28643ac7 | ||
|
|
9135d29346 | ||
|
|
96af524aad | ||
|
|
3a9a1c93dd | ||
|
|
b0618b9c9a | ||
|
|
c789490294 | ||
|
|
bf2fbfc52d | ||
|
|
3613edf321 | ||
|
|
b06c0f8ff9 | ||
|
|
05900fbabf | ||
|
|
51ae26128b | ||
|
|
1b66332f06 | ||
|
|
7b4b325e9d | ||
|
|
5c09cc37c9 | ||
|
|
e96e0e113a | ||
|
|
2d7ec43173 | ||
|
|
536da88df7 | ||
|
|
09b5838fda | ||
|
|
415fac82b2 | ||
|
|
eb30fb7330 | ||
|
|
be54e89ba6 | ||
|
|
5f17d281a6 | ||
|
|
d918f7e51b | ||
|
|
cb6cf107ba | ||
|
|
c9546c2419 | ||
|
|
2a1b2ed24d | ||
|
|
889df336d6 | ||
|
|
12d288ffcd | ||
|
|
4dd453f92c | ||
|
|
e42be00054 | ||
|
|
c3491ebdc3 | ||
|
|
f39f103dbe | ||
|
|
cf06c25904 | ||
|
|
dde92d1269 | ||
|
|
46455e854e | ||
|
|
473d391ea0 | ||
|
|
1bc71b8f9e | ||
|
|
4773ffba58 | ||
|
|
b5c5e807ea | ||
|
|
471dbace44 | ||
|
|
507e1039a4 | ||
|
|
a8d5070e98 | ||
|
|
9e5e76080d | ||
|
|
e40488b56e | ||
|
|
e5629321db | ||
|
|
4cb755d375 | ||
|
|
9eb59946ce | ||
|
|
a623616629 | ||
|
|
798b3bc158 | ||
|
|
18044abbb6 | ||
|
|
6e515b00ca | ||
|
|
96a949189d | ||
|
|
9bd4385002 | ||
|
|
f0a85acb63 | ||
|
|
7fc0525577 | ||
|
|
18150fa70f | ||
|
|
ffea61b540 | ||
|
|
d6350c672a | ||
|
|
6176e16275 | ||
|
|
47e621aaf3 | ||
|
|
5e84692e7e | ||
|
|
6ab541ea4e | ||
|
|
034f427638 | ||
|
|
143036aa0a | ||
|
|
ec001a067f | ||
|
|
41239caa97 | ||
|
|
30718a3be5 | ||
|
|
ca44da8bdf | ||
|
|
4d6ca07596 | ||
|
|
ba6a815711 | ||
|
|
9ac77140b6 | ||
|
|
0bcfdd2c07 | ||
|
|
b1f4606231 | ||
|
|
8ec024a8fd | ||
|
|
6c6290d20f | ||
|
|
707fbcf649 | ||
|
|
fea3cc2564 | ||
|
|
1251750403 | ||
|
|
40045ae02d | ||
|
|
ae28139206 | ||
|
|
c6b0e0cee3 | ||
|
|
31644f1542 | ||
|
|
aeba695431 | ||
|
|
e7165e8b6e | ||
|
|
392821700c | ||
|
|
133e98f7e1 | ||
|
|
52a0982948 | ||
|
|
ad0116ac0b | ||
|
|
05487c9f66 | ||
|
|
e121f8e81b | ||
|
|
94dc7bb279 | ||
|
|
93613ec5e1 | ||
|
|
5089cb045e | ||
|
|
fa26b6815d | ||
|
|
cde5527c9f | ||
|
|
0674aa7d8f | ||
|
|
c56da459bc | ||
|
|
77d9c8dcf0 | ||
|
|
cae29ab96e | ||
|
|
8bc32ebe5e | ||
|
|
f461d4099c | ||
|
|
32d16803a6 | ||
|
|
302149e02d | ||
|
|
f2b4ed2893 | ||
|
|
73b1d4d877 | ||
|
|
14afeb0e53 | ||
|
|
68ddbf82e0 | ||
|
|
c020c6bbf7 | ||
|
|
b7ab716ee5 | ||
|
|
b47eccbff0 | ||
|
|
f798d2df92 | ||
|
|
bf5aa56dc0 | ||
|
|
0609686803 | ||
|
|
2a41320963 | ||
|
|
f5d4c9fc1c | ||
|
|
23d6233d42 | ||
|
|
e169d97713 | ||
|
|
1862d1d827 | ||
|
|
07bad48dbc | ||
|
|
a05842d051 | ||
|
|
d8879486bd | ||
|
|
e4292702aa | ||
|
|
59f1e59ff1 | ||
|
|
fa3010a50e | ||
|
|
11d700d0a7 | ||
|
|
5c351c8d17 | ||
|
|
f14ac7146c | ||
|
|
69280ee64f | ||
|
|
f66ab39a40 | ||
|
|
0cea9346e1 | ||
|
|
1a7df66e10 | ||
|
|
7e6c33152e | ||
|
|
e48ca2892b | ||
|
|
5d6ce6c986 | ||
|
|
7b08953c18 | ||
|
|
3d1a74dda9 | ||
|
|
877ba28d85 | ||
|
|
f68a80879e | ||
|
|
7b0ab173dd | ||
|
|
4a5534f330 | ||
|
|
010640ccc8 | ||
|
|
f6c09da968 | ||
|
|
2c21ecd1df | ||
|
|
e9bc79d5ff | ||
|
|
44599e9e7e | ||
|
|
910fe62d92 | ||
|
|
5a1beaffb8 | ||
|
|
bdf5434d7b | ||
|
|
096a3dc645 | ||
|
|
11aefa3e42 | ||
|
|
f7eb51204a | ||
|
|
3fde8f3b27 | ||
|
|
1795740517 | ||
|
|
4783df5737 | ||
|
|
b00e63e37e | ||
|
|
322165f404 | ||
|
|
074e4b3144 | ||
|
|
bd51adebdf | ||
|
|
afffb13902 | ||
|
|
afe5e96aee | ||
|
|
95f3c0d69d | ||
|
|
185111d197 | ||
|
|
439b44ee93 | ||
|
|
d39ddac486 | ||
|
|
f3a8e98511 | ||
|
|
703ab1eee0 | ||
|
|
d3028e0227 | ||
|
|
fe87ca8117 | ||
|
|
fa39705dcb | ||
|
|
e53c9d1540 | ||
|
|
8d3ca534c4 | ||
|
|
6f8a652653 | ||
|
|
cfed9f3958 | ||
|
|
44da572893 | ||
|
|
57b0ca644e | ||
|
|
f750c0ca06 | ||
|
|
25ca331dc8 | ||
|
|
a209c1a7dc | ||
|
|
0873e451a8 | ||
|
|
0eea760443 | ||
|
|
62ae7339fe | ||
|
|
887ffd1116 | ||
|
|
d5975a4023 | ||
|
|
057d6aa497 | ||
|
|
395e7b390a | ||
|
|
c36e3612e7 | ||
|
|
8b6ef48046 | ||
|
|
31c33c87c5 | ||
|
|
8c9a33b737 | ||
|
|
286810d25b | ||
|
|
8c5a279ba2 | ||
|
|
c7081f90e8 | ||
|
|
6e921912d7 | ||
|
|
c132c66ac6 | ||
|
|
e3bf553dc3 | ||
|
|
7c3115ded6 | ||
|
|
db2fa98b52 | ||
|
|
42ee90b4d5 | ||
|
|
f5d2b869c0 | ||
|
|
919a2fbfda | ||
|
|
09c2dc2e85 | ||
|
|
a3c1082bb1 | ||
|
|
3a13a7bfda | ||
|
|
1a698fae33 | ||
|
|
0772f6fe16 | ||
|
|
f63e6f3e1b | ||
|
|
b636979127 | ||
|
|
55bb584360 | ||
|
|
5cce71db44 | ||
|
|
adb8d08995 | ||
|
|
6baddab4da | ||
|
|
5e51f2a6ed | ||
|
|
701efe8903 | ||
|
|
791111b060 | ||
|
|
999925744c | ||
|
|
7188e0486a | ||
|
|
993c892ad9 | ||
|
|
92b1a5bf3e | ||
|
|
4993925471 | ||
|
|
11e69eab36 | ||
|
|
e22ff3af36 | ||
|
|
7a5053dfb2 | ||
|
|
4d04e5522c | ||
|
|
ff1d4a1d4e | ||
|
|
7a6848595f | ||
|
|
d39b44c4d3 | ||
|
|
6a4ce0a620 | ||
|
|
494c7bda58 | ||
|
|
8000235591 | ||
|
|
64e1df5cdb | ||
|
|
3817246915 | ||
|
|
c5ce44eaaa | ||
|
|
75e11daa95 | ||
|
|
91b06fbe03 | ||
|
|
fb7393e4bb | ||
|
|
0a8c642e93 | ||
|
|
6b848f5038 | ||
|
|
eee6e78ec1 | ||
|
|
9c3ced06d5 | ||
|
|
6a45c3e25a | ||
|
|
d6acd2ee7d | ||
|
|
ee78527a9e | ||
|
|
b08f415f80 | ||
|
|
80afab57d3 | ||
|
|
12297982c8 | ||
|
|
7da7929edf | ||
|
|
aad90154e5 | ||
|
|
7b99df6c48 | ||
|
|
70fb78d031 | ||
|
|
d41e46be14 | ||
|
|
73364fa2db | ||
|
|
44f6446f6c | ||
|
|
dfb37d9a44 | ||
|
|
ab1de2942c | ||
|
|
2ff3ae86d0 | ||
|
|
906dfd3b6c | ||
|
|
a824e7b379 | ||
|
|
a2b358613b | ||
|
|
99b7a5b482 | ||
|
|
725dac204f | ||
|
|
ccc8f7ed37 | ||
|
|
8d38bf8ded | ||
|
|
cdfc67661e | ||
|
|
4fba939726 | ||
|
|
cf2b48fc0c | ||
|
|
962b3659c6 | ||
|
|
717242636a | ||
|
|
29055ab4a6 | ||
|
|
29073cf711 | ||
|
|
cf82d6f0a5 | ||
|
|
8fd984a767 | ||
|
|
1db415d897 | ||
|
|
914e14bb8d | ||
|
|
c8eff6ec07 | ||
|
|
107a64003d | ||
|
|
47d2e7b658 | ||
|
|
13b4fd9b9f | ||
|
|
e95eafe02a | ||
|
|
2cc83ed1fe | ||
|
|
b052d646ee | ||
|
|
174ef52df7 | ||
|
|
1fda3ba969 | ||
|
|
f314328294 | ||
|
|
0f24d66234 | ||
|
|
fd028253fe | ||
|
|
2e4ff723aa | ||
|
|
1218ddd5dc | ||
|
|
330eaf3ed6 | ||
|
|
2758a0ec53 | ||
|
|
1f63fc4fea | ||
|
|
de481bcd67 | ||
|
|
12c96d06e4 | ||
|
|
b572b91783 | ||
|
|
ad29fbdd55 | ||
|
|
f21ad9b879 | ||
|
|
8851a246de | ||
|
|
23cfdfffba | ||
|
|
f5f0d1a43b | ||
|
|
8bfbfe3e46 | ||
|
|
21615cb158 | ||
|
|
b6b6766000 | ||
|
|
f37ec60456 | ||
|
|
b35861005e | ||
|
|
123559478e | ||
|
|
bbd8fb2342 | ||
|
|
310a83427c | ||
|
|
0b6546aa8d | ||
|
|
21afc1933a | ||
|
|
e800af70bb | ||
|
|
40d36165a4 | ||
|
|
f10b76f087 | ||
|
|
2efc566b14 | ||
|
|
089c8b7b0a | ||
|
|
134d9f1e98 | ||
|
|
7e0863ef81 | ||
|
|
e96dd96864 | ||
|
|
8ff947e83a | ||
|
|
d5fd7ff339 | ||
|
|
8a223b8178 | ||
|
|
19193cd2a4 | ||
|
|
c15f670f2c | ||
|
|
f583f9a2d4 | ||
|
|
70734a1f28 | ||
|
|
b458797979 | ||
|
|
754b1df51d | ||
|
|
0da079c98c | ||
|
|
7be91fc92c | ||
|
|
415cdb1ef9 | ||
|
|
17c5361d43 | ||
|
|
6cf99b4415 | ||
|
|
7a5d4ecdc9 | ||
|
|
1a7792e06f | ||
|
|
95e863cb6d | ||
|
|
9b55505cdf | ||
|
|
b687960c5d | ||
|
|
10ccbc6a31 | ||
|
|
0e2e7e533a | ||
|
|
d71910cdc2 | ||
|
|
3cb7c61026 | ||
|
|
08c73444c4 | ||
|
|
e686b64ff3 | ||
|
|
7d1fae001b | ||
|
|
f190cdb3ec | ||
|
|
6275435567 | ||
|
|
5a90b14099 | ||
|
|
bca7c16435 | ||
|
|
925962505e | ||
|
|
e95529aca0 | ||
|
|
ed7bcf787c | ||
|
|
87728c4452 | ||
|
|
134f62a776 | ||
|
|
8fb53964e4 | ||
|
|
eb7daf1b74 | ||
|
|
ea665cb743 | ||
|
|
2287720201 | ||
|
|
91b57a3f28 | ||
|
|
97d69bd437 | ||
|
|
3c4ac0e5a2 | ||
|
|
9822749c92 | ||
|
|
d9fc95c2a1 | ||
|
|
788ccffb2b | ||
|
|
1fea829760 | ||
|
|
53b89b704c | ||
|
|
a949c35228 | ||
|
|
81fbcbc1b2 | ||
|
|
05317f1664 | ||
|
|
8a42a68a4f | ||
|
|
984db6798c | ||
|
|
ba9c354e1b | ||
|
|
f1dc0a6fcb | ||
|
|
13d715e2a5 | ||
|
|
1fceeab0fc | ||
|
|
51020cd635 | ||
|
|
4d05077ec3 | ||
|
|
5b6a4c4563 | ||
|
|
8e834cbdfc | ||
|
|
27233364bc | ||
|
|
685532d44a | ||
|
|
692c8036b7 | ||
|
|
f9715c442a | ||
|
|
e73a1c3298 | ||
|
|
129a6f187e | ||
|
|
7710ed00d3 | ||
|
|
fcc17254f6 | ||
|
|
6edd3705a6 | ||
|
|
58f988bf33 | ||
|
|
34a0615ac1 | ||
|
|
5a74f08c84 | ||
|
|
bdce56df30 | ||
|
|
f4a7b652c7 | ||
|
|
cb479bf199 | ||
|
|
b10421a9b4 | ||
|
|
ef77e45427 | ||
|
|
823b5288f9 | ||
|
|
c17951e3f6 | ||
|
|
f0a29de87d | ||
|
|
b60271a6cf | ||
|
|
f00e6b69f2 | ||
|
|
cc8515041b | ||
|
|
01512e27a0 | ||
|
|
301b79c688 | ||
|
|
98654634ca | ||
|
|
1812bd17a5 | ||
|
|
7b6486cd30 | ||
|
|
223fe14d85 | ||
|
|
5eb775462d | ||
|
|
ed60687f11 | ||
|
|
66d9514e12 | ||
|
|
37cc852bfc | ||
|
|
10eb20e44b | ||
|
|
7744e95aea | ||
|
|
3f871f0805 | ||
|
|
db7b945262 | ||
|
|
64aef5c13c | ||
|
|
3fe5a368ac | ||
|
|
f23a8480e7 | ||
|
|
5d6a48b568 | ||
|
|
df48df5ede | ||
|
|
51fcfe980f | ||
|
|
e983506e23 | ||
|
|
456bc80697 | ||
|
|
5be22a073b | ||
|
|
26a3ec9ae3 | ||
|
|
8d6d47a5c6 | ||
|
|
5adae77e39 | ||
|
|
79840126b9 | ||
|
|
05d0248b18 | ||
|
|
2a61e1cace | ||
|
|
abf850af91 | ||
|
|
0722db10df | ||
|
|
1fece09687 | ||
|
|
9f0db9a02a | ||
|
|
6f499013f6 | ||
|
|
29de419c87 | ||
|
|
9bde9c9fbb | ||
|
|
865c4984f6 | ||
|
|
987ad12181 | ||
|
|
003f3b4fe1 | ||
|
|
18f6a659f4 | ||
|
|
6e0de0d196 | ||
|
|
4d464cc9fb | ||
|
|
4030807b9b | ||
|
|
174fbe56b0 | ||
|
|
ab7971de42 | ||
|
|
65224213d0 | ||
|
|
992f5aaed7 | ||
|
|
298b169dc0 | ||
|
|
3fd46e8cbe | ||
|
|
9c10d4aa3f | ||
|
|
a7b8768e56 | ||
|
|
5d482abb67 | ||
|
|
1871a1632e | ||
|
|
4e19be7e43 | ||
|
|
ff973caa67 | ||
|
|
362be9f344 | ||
|
|
904e3a3492 | ||
|
|
404c398b59 | ||
|
|
1f92212497 | ||
|
|
d7f65cbbcd | ||
|
|
b4124d0d92 | ||
|
|
f3e9413b2d | ||
|
|
d09527b75e | ||
|
|
ad05321d94 | ||
|
|
822f6a9fa6 | ||
|
|
b846311173 | ||
|
|
cdf5634cf2 | ||
|
|
2d02933d35 | ||
|
|
11991772f8 | ||
|
|
ab9573f108 | ||
|
|
195673b01a | ||
|
|
c656cdd951 | ||
|
|
1bec1d5f26 | ||
|
|
7a12d1e322 | ||
|
|
25ddf1d1e9 | ||
|
|
6e4c2fe786 | ||
|
|
a7c9856851 | ||
|
|
78382209d3 | ||
|
|
c32bfa6eeb | ||
|
|
815083d1e8 | ||
|
|
b432bb0cf3 | ||
|
|
22bc2d24ab | ||
|
|
e5cf139b03 | ||
|
|
ec7b5861f9 | ||
|
|
58ac82b0a7 | ||
|
|
6d1f2986fe | ||
|
|
166959db41 | ||
|
|
367a948ff0 | ||
|
|
6d996834f3 | ||
|
|
e731e658ba | ||
|
|
f62e8f594d | ||
|
|
880ab910f7 | ||
|
|
2da4b70e3a | ||
|
|
f58014e2b3 | ||
|
|
dbfa61a156 | ||
|
|
8294d4ae8e | ||
|
|
8daffa76dd | ||
|
|
d55f554d7b | ||
|
|
a0472e11a9 | ||
|
|
c8d34fbe8c | ||
|
|
6ccb9814f8 | ||
|
|
733813700e | ||
|
|
af1d46f184 | ||
|
|
5f7fffbe81 | ||
|
|
71e11950d9 | ||
|
|
a1b74b0758 | ||
|
|
f2515405b5 | ||
|
|
3dc15623d5 | ||
|
|
ffe1b6e03a | ||
|
|
7265921719 | ||
|
|
58e24cea4b | ||
|
|
aa8336ee94 | ||
|
|
733ff9a05b | ||
|
|
1204996378 | ||
|
|
c0f5266bc4 | ||
|
|
1ca57407d3 | ||
|
|
e082d8cc75 | ||
|
|
94237df7b5 | ||
|
|
9377bef603 | ||
|
|
6ee827bd8a | ||
|
|
351d464448 | ||
|
|
fcacdb2791 | ||
|
|
e25b89d49a | ||
|
|
6385b82c2b | ||
|
|
622b62cff1 | ||
|
|
e206505061 | ||
|
|
4955f05fa3 | ||
|
|
258d1c0521 | ||
|
|
734b872e41 | ||
|
|
8405693325 | ||
|
|
910574361c | ||
|
|
0a8fa3a8ba | ||
|
|
0094064f4a | ||
|
|
7e37f8e3ed | ||
|
|
945a53353f | ||
|
|
e3111729c6 | ||
|
|
d41c786554 | ||
|
|
c40e26ada9 | ||
|
|
9a8364566c | ||
|
|
75aefb89fd | ||
|
|
a3e60af35e | ||
|
|
bc87fadc03 | ||
|
|
76e295f606 | ||
|
|
ad7544ce90 | ||
|
|
01a5355713 | ||
|
|
abcb93d0bd | ||
|
|
e45b5b35e3 | ||
|
|
a6e44a349a | ||
|
|
fe22f1e46a | ||
|
|
89bb079e50 | ||
|
|
f2e32857cb | ||
|
|
7492b46c43 | ||
|
|
ad20a404a9 | ||
|
|
496006d629 | ||
|
|
f2f8ecac73 | ||
|
|
204643b8da | ||
|
|
4f206c5f1e | ||
|
|
1e09c63282 | ||
|
|
c88248acb1 | ||
|
|
547d752df5 | ||
|
|
8b40c2e372 | ||
|
|
f51764f8d0 | ||
|
|
64f95fec69 | ||
|
|
8ca35fdf4f | ||
|
|
d87921c12a | ||
|
|
ae3de43a62 | ||
|
|
fbb5e78176 | ||
|
|
10de265b4f | ||
|
|
4a357ee6e9 | ||
|
|
40b96bfffd | ||
|
|
f53c68695d | ||
|
|
cfc2e3e228 | ||
|
|
25398fe622 | ||
|
|
a7c3740dee | ||
|
|
7de056437f | ||
|
|
cfa9de4047 | ||
|
|
c48fe567f7 | ||
|
|
c4b6789666 | ||
|
|
6404c4bca8 | ||
|
|
9423712696 | ||
|
|
5018b1a426 | ||
|
|
1336ca5d9d | ||
|
|
5e3046d86a | ||
|
|
e45536ae93 | ||
|
|
48254acb4e | ||
|
|
20b408ff3e | ||
|
|
4a579b5752 | ||
|
|
edf5995a1e | ||
|
|
6b2efdc9c3 | ||
|
|
17f5aed6dd | ||
|
|
fb44ded002 | ||
|
|
fea711cebb | ||
|
|
57efe6f75d | ||
|
|
6d71f75817 | ||
|
|
d570b1731e | ||
|
|
8fc263f62f | ||
|
|
3886f889ab | ||
|
|
f96942bdc0 | ||
|
|
f628a3fd7e | ||
|
|
f078f79c90 | ||
|
|
af45d22fb7 | ||
|
|
c3a7d8c433 | ||
|
|
6c7d81c325 | ||
|
|
a6f8c53924 | ||
|
|
6d5bbd913c | ||
|
|
4e957d3484 | ||
|
|
454ec3e74c | ||
|
|
ddf497623a | ||
|
|
746cb0493f | ||
|
|
2e27587f15 | ||
|
|
84c27f3694 | ||
|
|
42c0752370 | ||
|
|
3615b1620e | ||
|
|
e42553867f | ||
|
|
955347e426 | ||
|
|
468f1aa312 | ||
|
|
afb64c873a | ||
|
|
8f17468f51 | ||
|
|
78b58122c1 | ||
|
|
97ac351c5e | ||
|
|
c7f442425d | ||
|
|
df0963446f | ||
|
|
ef873b4b60 | ||
|
|
6a267debf5 | ||
|
|
334bb30f9e | ||
|
|
da9b2ee6b9 | ||
|
|
3ebf23e39b | ||
|
|
b5f54f1624 | ||
|
|
33da5bf81f | ||
|
|
43e3fd9c2c | ||
|
|
0201a5aadb | ||
|
|
fa38e9fa47 | ||
|
|
382df8e714 | ||
|
|
c5db729191 | ||
|
|
e0a294849a | ||
|
|
fcfe8e1ac8 | ||
|
|
4f1085beb3 | ||
|
|
e325a0003b | ||
|
|
e967e5ed11 | ||
|
|
d4aa062441 | ||
|
|
e5735cde67 | ||
|
|
75f3f3c8c4 | ||
|
|
3b8d670c81 | ||
|
|
8b7158f169 | ||
|
|
82c1a3a9ea | ||
|
|
53d0f41ba5 | ||
|
|
a5091c56cb | ||
|
|
daca1f38cf | ||
|
|
5df8d29fe4 | ||
|
|
8c38a8381c | ||
|
|
d22446d1dd | ||
|
|
c037e17220 | ||
|
|
506e17ace2 | ||
|
|
e88f09842d | ||
|
|
e79b1398b0 | ||
|
|
371b5e2a6e | ||
|
|
a4629db520 | ||
|
|
aaa364da54 | ||
|
|
a0c34dab54 | ||
|
|
1d815464f6 | ||
|
|
461f7491e8 | ||
|
|
abe8cfdf1c | ||
|
|
d9e3f9ca68 | ||
|
|
e08252cad7 | ||
|
|
9206ad52b8 | ||
|
|
57fd8c97b4 | ||
|
|
f9f665be62 | ||
|
|
3f1fb9d29d | ||
|
|
8afe853b76 | ||
|
|
5ec62358b3 | ||
|
|
dcda3e43b9 | ||
|
|
81fc6f16cf | ||
|
|
07f4218bf4 | ||
|
|
529d223ee1 | ||
|
|
3c4f04f3aa | ||
|
|
1fd557d549 | ||
|
|
9cac89347e | ||
|
|
020372dde0 | ||
|
|
bf49952275 | ||
|
|
491bd0e74f | ||
|
|
5c43cab499 | ||
|
|
317802a26e | ||
|
|
4bd090887e | ||
|
|
2fc0edf85b | ||
|
|
4192e5de2d | ||
|
|
443835096b | ||
|
|
d3855a9158 | ||
|
|
1d1ff8db7a | ||
|
|
937b0d3419 | ||
|
|
96bfe406ee | ||
|
|
271bfcbac7 | ||
|
|
5e40168829 | ||
|
|
bc9e4abee5 | ||
|
|
8d28a077fc | ||
|
|
61b48f1089 | ||
|
|
d4207e2936 | ||
|
|
65a62b8d32 | ||
|
|
cf54a780cf | ||
|
|
6d3cbf192e | ||
|
|
d94d0ac720 | ||
|
|
3942a08e0c | ||
|
|
3092c17080 | ||
|
|
b8a88719a7 | ||
|
|
e22d91b8c4 | ||
|
|
5adc09150d | ||
|
|
f3a0127715 | ||
|
|
b8d95537e0 | ||
|
|
398c44e334 | ||
|
|
74284c3271 | ||
|
|
1e9759679f | ||
|
|
e4096fb6f2 | ||
|
|
a3c372f602 | ||
|
|
42a22c280d | ||
|
|
adc23253e4 | ||
|
|
5025113da0 | ||
|
|
5fb03b6891 | ||
|
|
0fcac14fd7 | ||
|
|
7edf3c86c3 | ||
|
|
ecac89521b | ||
|
|
51ba929fc6 | ||
|
|
b6e4471458 | ||
|
|
763eaed2af | ||
|
|
e1ab8a9b42 | ||
|
|
daa9f116d0 | ||
|
|
c0ab8e8256 | ||
|
|
6d927ef83d | ||
|
|
78e815c861 | ||
|
|
8284a7cb89 | ||
|
|
03f0e717a7 | ||
|
|
778b8fdb2b | ||
|
|
d0bf163641 | ||
|
|
685a7e8ac5 | ||
|
|
a866d881e5 | ||
|
|
0527c4eae6 | ||
|
|
bdd3beb257 | ||
|
|
3b4b134328 | ||
|
|
4f38cb68e4 | ||
|
|
3d52769a47 | ||
|
|
8cb426120b | ||
|
|
9e8e5a1e81 | ||
|
|
88e3c83138 | ||
|
|
62aa942bf5 | ||
|
|
fc0b08042d | ||
|
|
225efcaf02 | ||
|
|
115357ddd1 | ||
|
|
a3507e7f58 | ||
|
|
189a490e27 | ||
|
|
52afc1ace5 | ||
|
|
f0c427b2d2 | ||
|
|
ff90fe52ee | ||
|
|
02ee5bcb6a | ||
|
|
16c1e214d6 | ||
|
|
019be59079 | ||
|
|
84caa50a08 | ||
|
|
59b191c789 | ||
|
|
6e7be1ec8d | ||
|
|
f4363c8a86 | ||
|
|
211bdb0bb7 | ||
|
|
c3709e8b04 | ||
|
|
c79b1fa87a | ||
|
|
964cc1b206 | ||
|
|
b31b08a30e | ||
|
|
15228f4463 | ||
|
|
e4102aae8d | ||
|
|
ac4fc783ae | ||
|
|
09ff71105a | ||
|
|
290736ae06 | ||
|
|
ea9db0ebbe | ||
|
|
82892f3e0e | ||
|
|
1640b6a7f3 | ||
|
|
fdd3beaf58 | ||
|
|
ae770996f6 | ||
|
|
cdb1323515 | ||
|
|
49b682edca | ||
|
|
e1f41e83d1 | ||
|
|
acbc517b55 | ||
|
|
253b62f8b0 | ||
|
|
cb97a80350 | ||
|
|
84897d4834 | ||
|
|
a9696d8501 | ||
|
|
8828fd17a0 | ||
|
|
aa3d24ea2a | ||
|
|
68d413e923 | ||
|
|
ee5a1c9002 | ||
|
|
2861c874ed | ||
|
|
91a897dd74 | ||
|
|
e64faa781c | ||
|
|
9bbe745a33 | ||
|
|
658d5a8b7e | ||
|
|
4e6b27144a | ||
|
|
26a78dbaa4 | ||
|
|
7e7b973481 | ||
|
|
646c8ac657 | ||
|
|
236631141f | ||
|
|
c4f10edc95 | ||
|
|
e42df1d859 | ||
|
|
18258000cd | ||
|
|
c494feb7f7 | ||
|
|
de8973d77a | ||
|
|
942063d4ef | ||
|
|
c15387e972 | ||
|
|
c31462d51b | ||
|
|
8b1fcea7ec | ||
|
|
a48f5b07c5 | ||
|
|
ca20b5951d | ||
|
|
f3b30443aa | ||
|
|
ccae898885 | ||
|
|
982d8f53f2 | ||
|
|
34bde45a2c | ||
|
|
9058f28788 | ||
|
|
bf57f289bf | ||
|
|
4007f82765 | ||
|
|
919e1d7933 | ||
|
|
74ebdaf4e8 | ||
|
|
c5871f4c2a | ||
|
|
58981a41e9 | ||
|
|
86435b8a4b | ||
|
|
980863366c | ||
|
|
7324feef89 | ||
|
|
5bd6b672d0 | ||
|
|
869d80a34b | ||
|
|
a3c3573d67 | ||
|
|
b4f0b05455 | ||
|
|
df8453d387 | ||
|
|
73abbace85 | ||
|
|
d2b48fdea2 | ||
|
|
caef94d851 | ||
|
|
9728dd8699 | ||
|
|
b497b2234d | ||
|
|
211a66cf33 | ||
|
|
ca1346ee03 | ||
|
|
2a43d66e11 | ||
|
|
65c477414b | ||
|
|
39a648bce1 | ||
|
|
11532c5be9 | ||
|
|
cc4441b50f | ||
|
|
174c9326a4 | ||
|
|
2473c3e49f | ||
|
|
2592067f95 | ||
|
|
9c95994dab | ||
|
|
db5bd646b9 | ||
|
|
4416158ece | ||
|
|
7f0dad7901 | ||
|
|
6a792324b0 | ||
|
|
f2311b56fc | ||
|
|
84113cba2c | ||
|
|
97000fc4e0 | ||
|
|
13a8b6cc4e | ||
|
|
76c95abbb5 | ||
|
|
33ef823645 | ||
|
|
817eb66167 | ||
|
|
f05f6cb44d | ||
|
|
36433a9f4d | ||
|
|
db84f32981 | ||
|
|
af1a53cb05 | ||
|
|
8fba935bba | ||
|
|
8c442f599b | ||
|
|
04bb80f157 | ||
|
|
f7287553e9 | ||
|
|
ae44580371 | ||
|
|
fa2adaf2ff | ||
|
|
af7901dcb2 | ||
|
|
5a4459856c | ||
|
|
93c2b8b555 | ||
|
|
0b2c8d1fa2 | ||
|
|
312e5b8756 | ||
|
|
aa0f20b93e | ||
|
|
78485fd8df | ||
|
|
4e03fb361f | ||
|
|
82922bf0d7 | ||
|
|
e1362ead3c | ||
|
|
c2ee9ca3e0 |
59
.cspell.dict/cpython.txt
Normal file
59
.cspell.dict/cpython.txt
Normal file
@@ -0,0 +1,59 @@
|
||||
argtypes
|
||||
asdl
|
||||
asname
|
||||
augassign
|
||||
badsyntax
|
||||
basetype
|
||||
boolop
|
||||
bxor
|
||||
cached_tsver
|
||||
cellarg
|
||||
cellvar
|
||||
cellvars
|
||||
cmpop
|
||||
denom
|
||||
dictoffset
|
||||
elts
|
||||
excepthandler
|
||||
fileutils
|
||||
finalbody
|
||||
formatfloat
|
||||
freevar
|
||||
freevars
|
||||
fromlist
|
||||
heaptype
|
||||
HIGHRES
|
||||
IMMUTABLETYPE
|
||||
kwonlyarg
|
||||
kwonlyargs
|
||||
lasti
|
||||
linearise
|
||||
maxdepth
|
||||
mult
|
||||
nkwargs
|
||||
noraise
|
||||
numer
|
||||
orelse
|
||||
pathconfig
|
||||
patma
|
||||
posonlyarg
|
||||
posonlyargs
|
||||
prec
|
||||
preinitialized
|
||||
PYTHREAD_NAME
|
||||
SA_ONSTACK
|
||||
stackdepth
|
||||
stringlib
|
||||
structseq
|
||||
tok_oldval
|
||||
unaryop
|
||||
unparse
|
||||
unparser
|
||||
VARKEYWORDS
|
||||
varkwarg
|
||||
wbits
|
||||
weakreflist
|
||||
withitem
|
||||
withs
|
||||
xstat
|
||||
XXPRIME
|
||||
257
.cspell.dict/python-more.txt
Normal file
257
.cspell.dict/python-more.txt
Normal file
@@ -0,0 +1,257 @@
|
||||
abiflags
|
||||
abstractmethods
|
||||
aenter
|
||||
aexit
|
||||
aiter
|
||||
anext
|
||||
appendleft
|
||||
argcount
|
||||
arrayiterator
|
||||
arraytype
|
||||
asend
|
||||
asyncgen
|
||||
athrow
|
||||
backslashreplace
|
||||
baserepl
|
||||
basicsize
|
||||
bdfl
|
||||
bigcharset
|
||||
bignum
|
||||
breakpointhook
|
||||
cformat
|
||||
chunksize
|
||||
classcell
|
||||
closefd
|
||||
closesocket
|
||||
codepoint
|
||||
codepoints
|
||||
codesize
|
||||
contextvar
|
||||
cpython
|
||||
cratio
|
||||
dealloc
|
||||
debugbuild
|
||||
decompressor
|
||||
defaultaction
|
||||
descr
|
||||
dictcomp
|
||||
dictitems
|
||||
dictkeys
|
||||
dictview
|
||||
digestmod
|
||||
dllhandle
|
||||
docstring
|
||||
docstrings
|
||||
dunder
|
||||
endianness
|
||||
endpos
|
||||
eventmask
|
||||
excepthook
|
||||
exceptiongroup
|
||||
exitfuncs
|
||||
extendleft
|
||||
fastlocals
|
||||
fdel
|
||||
fedcba
|
||||
fget
|
||||
fileencoding
|
||||
fillchar
|
||||
fillvalue
|
||||
finallyhandler
|
||||
firstiter
|
||||
firstlineno
|
||||
fnctl
|
||||
frombytes
|
||||
fromhex
|
||||
fromunicode
|
||||
fset
|
||||
fspath
|
||||
fstring
|
||||
fstrings
|
||||
ftruncate
|
||||
genexpr
|
||||
getattro
|
||||
getcodesize
|
||||
getdefaultencoding
|
||||
getfilesystemencodeerrors
|
||||
getfilesystemencoding
|
||||
getformat
|
||||
getframe
|
||||
getnewargs
|
||||
getpip
|
||||
getrandom
|
||||
getrecursionlimit
|
||||
getrefcount
|
||||
getsizeof
|
||||
getweakrefcount
|
||||
getweakrefs
|
||||
getwindowsversion
|
||||
gmtoff
|
||||
groupdict
|
||||
groupindex
|
||||
hamt
|
||||
hostnames
|
||||
idfunc
|
||||
idiv
|
||||
idxs
|
||||
impls
|
||||
indexgroup
|
||||
infj
|
||||
instancecheck
|
||||
instanceof
|
||||
irepeat
|
||||
isabstractmethod
|
||||
isbytes
|
||||
iscased
|
||||
isfinal
|
||||
istext
|
||||
itemiterator
|
||||
itemsize
|
||||
iternext
|
||||
keepends
|
||||
keyfunc
|
||||
keyiterator
|
||||
kwarg
|
||||
kwargs
|
||||
kwdefaults
|
||||
kwonlyargcount
|
||||
lastgroup
|
||||
lastindex
|
||||
linearization
|
||||
linearize
|
||||
listcomp
|
||||
longrange
|
||||
lvalue
|
||||
mappingproxy
|
||||
maskpri
|
||||
maxdigits
|
||||
MAXGROUPS
|
||||
MAXREPEAT
|
||||
maxsplit
|
||||
maxunicode
|
||||
memoryview
|
||||
memoryviewiterator
|
||||
metaclass
|
||||
metaclasses
|
||||
metatype
|
||||
mformat
|
||||
mro
|
||||
mros
|
||||
multiarch
|
||||
namereplace
|
||||
nanj
|
||||
nbytes
|
||||
ncallbacks
|
||||
ndigits
|
||||
ndim
|
||||
nldecoder
|
||||
nlocals
|
||||
NOARGS
|
||||
nonbytes
|
||||
Nonprintable
|
||||
origname
|
||||
ospath
|
||||
pendingcr
|
||||
phello
|
||||
platlibdir
|
||||
popleft
|
||||
posixsubprocess
|
||||
posonly
|
||||
posonlyargcount
|
||||
prepending
|
||||
profilefunc
|
||||
pycache
|
||||
pycodecs
|
||||
pycs
|
||||
pyexpat
|
||||
PYTHONBREAKPOINT
|
||||
PYTHONDEBUG
|
||||
PYTHONHASHSEED
|
||||
PYTHONHOME
|
||||
PYTHONINSPECT
|
||||
PYTHONOPTIMIZE
|
||||
PYTHONPATH
|
||||
PYTHONPATH
|
||||
PYTHONSAFEPATH
|
||||
PYTHONVERBOSE
|
||||
PYTHONWARNDEFAULTENCODING
|
||||
PYTHONWARNINGS
|
||||
pytraverse
|
||||
PYVENV
|
||||
qualname
|
||||
quotetabs
|
||||
radd
|
||||
rdiv
|
||||
rdivmod
|
||||
readall
|
||||
readbuffer
|
||||
reconstructor
|
||||
refcnt
|
||||
releaselevel
|
||||
reverseitemiterator
|
||||
reverseiterator
|
||||
reversekeyiterator
|
||||
reversevalueiterator
|
||||
rfloordiv
|
||||
rlshift
|
||||
rmod
|
||||
rpow
|
||||
rrshift
|
||||
rsub
|
||||
rtruediv
|
||||
rvalue
|
||||
scproxy
|
||||
seennl
|
||||
setattro
|
||||
setcomp
|
||||
setrecursionlimit
|
||||
showwarnmsg
|
||||
signum
|
||||
slotnames
|
||||
STACKLESS
|
||||
stacklevel
|
||||
stacksize
|
||||
startpos
|
||||
subclassable
|
||||
subclasscheck
|
||||
subclasshook
|
||||
suboffset
|
||||
suboffsets
|
||||
SUBPATTERN
|
||||
sumprod
|
||||
surrogateescape
|
||||
surrogatepass
|
||||
sysconf
|
||||
sysconfigdata
|
||||
sysvars
|
||||
teedata
|
||||
thisclass
|
||||
titlecased
|
||||
tkapp
|
||||
tobytes
|
||||
tolist
|
||||
toreadonly
|
||||
TPFLAGS
|
||||
tracefunc
|
||||
unimportable
|
||||
unionable
|
||||
unraisablehook
|
||||
unsliceable
|
||||
urandom
|
||||
valueiterator
|
||||
vararg
|
||||
varargs
|
||||
varnames
|
||||
warningregistry
|
||||
warnmsg
|
||||
warnoptions
|
||||
warnopts
|
||||
weaklist
|
||||
weakproxy
|
||||
weakrefs
|
||||
winver
|
||||
withdata
|
||||
xmlcharrefreplace
|
||||
xoptions
|
||||
xopts
|
||||
yieldfrom
|
||||
82
.cspell.dict/rust-more.txt
Normal file
82
.cspell.dict/rust-more.txt
Normal file
@@ -0,0 +1,82 @@
|
||||
ahash
|
||||
arrayvec
|
||||
bidi
|
||||
biguint
|
||||
bindgen
|
||||
bitflags
|
||||
bitor
|
||||
bstr
|
||||
byteorder
|
||||
byteset
|
||||
caseless
|
||||
chrono
|
||||
consts
|
||||
cranelift
|
||||
cstring
|
||||
datelike
|
||||
deserializer
|
||||
fdiv
|
||||
flamescope
|
||||
flate2
|
||||
fract
|
||||
getres
|
||||
hasher
|
||||
hexf
|
||||
hexversion
|
||||
idents
|
||||
illumos
|
||||
indexmap
|
||||
insta
|
||||
keccak
|
||||
lalrpop
|
||||
lexopt
|
||||
libc
|
||||
libloading
|
||||
libz
|
||||
longlong
|
||||
Manually
|
||||
maplit
|
||||
memmap
|
||||
memmem
|
||||
metas
|
||||
modpow
|
||||
msvc
|
||||
muldiv
|
||||
nanos
|
||||
nonoverlapping
|
||||
objclass
|
||||
peekable
|
||||
powc
|
||||
powf
|
||||
powi
|
||||
prepended
|
||||
punct
|
||||
replacen
|
||||
rmatch
|
||||
rposition
|
||||
rsplitn
|
||||
rustc
|
||||
rustfmt
|
||||
rustyline
|
||||
seedable
|
||||
seekfrom
|
||||
siphash
|
||||
siphasher
|
||||
splitn
|
||||
subsec
|
||||
thiserror
|
||||
timelike
|
||||
timsort
|
||||
trai
|
||||
ulonglong
|
||||
unic
|
||||
unistd
|
||||
unraw
|
||||
unsync
|
||||
wasip1
|
||||
wasip2
|
||||
wasmbind
|
||||
wasmtime
|
||||
widestring
|
||||
winapi
|
||||
winsock
|
||||
207
.cspell.json
207
.cspell.json
@@ -1,134 +1,135 @@
|
||||
// See: https://github.com/streetsidesoftware/cspell/tree/master/packages/cspell
|
||||
{
|
||||
"version": "0.2",
|
||||
"import": [
|
||||
"@cspell/dict-en_us/cspell-ext.json",
|
||||
// "@cspell/dict-cpp/cspell-ext.json",
|
||||
"@cspell/dict-python/cspell-ext.json",
|
||||
"@cspell/dict-rust/cspell-ext.json",
|
||||
"@cspell/dict-win32/cspell-ext.json",
|
||||
"@cspell/dict-shell/cspell-ext.json",
|
||||
],
|
||||
// language - current active spelling language
|
||||
"language": "en",
|
||||
// dictionaries - list of the names of the dictionaries to use
|
||||
"dictionaries": [
|
||||
"cpython", // Sometimes keeping same terms with cpython is easy
|
||||
"python-more", // Python API terms not listed in python
|
||||
"rust-more", // Rust API terms not listed in rust
|
||||
"en_US",
|
||||
"softwareTerms",
|
||||
"c",
|
||||
"cpp",
|
||||
"python",
|
||||
"python-custom",
|
||||
"rust",
|
||||
"unix",
|
||||
"posix",
|
||||
"winapi"
|
||||
"shell",
|
||||
"win32"
|
||||
],
|
||||
// dictionaryDefinitions - this list defines any custom dictionaries to use
|
||||
"dictionaryDefinitions": [],
|
||||
"dictionaryDefinitions": [
|
||||
{
|
||||
"name": "cpython",
|
||||
"path": "./.cspell.dict/cpython.txt"
|
||||
},
|
||||
{
|
||||
"name": "python-more",
|
||||
"path": "./.cspell.dict/python-more.txt"
|
||||
},
|
||||
{
|
||||
"name": "rust-more",
|
||||
"path": "./.cspell.dict/rust-more.txt"
|
||||
}
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/__pycache__/**",
|
||||
"Lib/**"
|
||||
],
|
||||
// words - list of words to be always considered correct
|
||||
"words": [
|
||||
// Rust
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"bindgen",
|
||||
"cstring",
|
||||
"chrono",
|
||||
"peekable",
|
||||
"lalrpop",
|
||||
"memmap",
|
||||
"Manually",
|
||||
"rustc",
|
||||
"unistd",
|
||||
"unic",
|
||||
// Python
|
||||
"cformat",
|
||||
"cpython",
|
||||
"fspath",
|
||||
"kwarg",
|
||||
"kwargs",
|
||||
"vararg",
|
||||
"varargs",
|
||||
"metaclass",
|
||||
"metaclasses",
|
||||
"fstring",
|
||||
"fstrings",
|
||||
"docstring",
|
||||
"docstrings",
|
||||
"fileencoding",
|
||||
"linearization",
|
||||
"linearize",
|
||||
"PYTHONDEBUG",
|
||||
"PYTHONINSPECT",
|
||||
"PYTHONPATH",
|
||||
"PYTHONHOME",
|
||||
"PYTHONPATH",
|
||||
"PYTHONVERBOSE",
|
||||
"PYTHONOPTIMIZE",
|
||||
"PYTHONWARNINGS",
|
||||
"basicsize",
|
||||
"itemsize",
|
||||
"getattro",
|
||||
"setattro",
|
||||
"iternext",
|
||||
"maxsplit",
|
||||
"fdel",
|
||||
"subclasscheck",
|
||||
"qualname",
|
||||
"eventmask",
|
||||
"instanceof",
|
||||
"abstractmethods",
|
||||
"aiter",
|
||||
"anext",
|
||||
"rdiv",
|
||||
"idiv",
|
||||
"ndim",
|
||||
"varnames",
|
||||
"getweakrefs",
|
||||
"getweakrefcount",
|
||||
"stacklevel",
|
||||
"MemoryView",
|
||||
"warningregistry",
|
||||
"defaultaction",
|
||||
"unraisablehook",
|
||||
"descr",
|
||||
"xopts",
|
||||
"warnopts",
|
||||
"weakproxy",
|
||||
"mappingproxy",
|
||||
// RustPython
|
||||
"RustPython",
|
||||
"RUSTPYTHONPATH",
|
||||
// RustPython terms
|
||||
"aiterable",
|
||||
"alnum",
|
||||
"baseclass",
|
||||
"boxvec",
|
||||
"Bytecode",
|
||||
"cfgs",
|
||||
"codegen",
|
||||
"coro",
|
||||
"dedentations",
|
||||
"dedents",
|
||||
"deduped",
|
||||
"downcasted",
|
||||
"dumpable",
|
||||
"emscripten",
|
||||
"excs",
|
||||
"finalizer",
|
||||
"GetSet",
|
||||
"groupref",
|
||||
"internable",
|
||||
"lossily",
|
||||
"makeunicodedata",
|
||||
"miri",
|
||||
"notrace",
|
||||
"openat",
|
||||
"pyarg",
|
||||
"pyarg",
|
||||
"pyargs",
|
||||
"pygetset",
|
||||
"pyobj",
|
||||
"pystr",
|
||||
"pyc",
|
||||
"pyref",
|
||||
"pyslot",
|
||||
"PyFunction",
|
||||
"PyMethod",
|
||||
"PyClassMethod",
|
||||
"PyStaticMethod",
|
||||
"PyProperty",
|
||||
"PyClass",
|
||||
"pyimpl",
|
||||
"pyarg",
|
||||
"PyModule",
|
||||
"pyast",
|
||||
"PyAttr",
|
||||
"PyResult",
|
||||
"PyObject",
|
||||
"pyc",
|
||||
"PyClass",
|
||||
"PyClassMethod",
|
||||
"PyException",
|
||||
"GetSet",
|
||||
"zelf",
|
||||
"wasi",
|
||||
"Bytecode",
|
||||
"PyFunction",
|
||||
"pygetset",
|
||||
"pyimpl",
|
||||
"pylib",
|
||||
"pymember",
|
||||
"PyMethod",
|
||||
"PyModule",
|
||||
"pyname",
|
||||
"pyobj",
|
||||
"PyObject",
|
||||
"pypayload",
|
||||
"PyProperty",
|
||||
"pyref",
|
||||
"PyResult",
|
||||
"pyslot",
|
||||
"PyStaticMethod",
|
||||
"pystone",
|
||||
"pystr",
|
||||
"pystruct",
|
||||
"pystructseq",
|
||||
"pytrace",
|
||||
"reducelib",
|
||||
"richcompare",
|
||||
"makeunicodedata",
|
||||
"unhashable",
|
||||
"unraisable",
|
||||
"RustPython",
|
||||
"significand",
|
||||
"struc",
|
||||
"summands", // plural of summand
|
||||
"sysmodule",
|
||||
"tracebacks",
|
||||
"typealiases",
|
||||
"Unconstructible",
|
||||
"posonlyargs",
|
||||
"kwonlyargs",
|
||||
"unconstructible",
|
||||
"unhashable",
|
||||
"uninit",
|
||||
"miri"
|
||||
"unraisable",
|
||||
"unresizable",
|
||||
"wasi",
|
||||
"zelf",
|
||||
// unix
|
||||
"CLOEXEC",
|
||||
"codeset",
|
||||
"endgrent",
|
||||
"gethrvtime",
|
||||
"getrusage",
|
||||
"nanosleep",
|
||||
"sigaction",
|
||||
"WRLCK",
|
||||
// win32
|
||||
"birthtime",
|
||||
"IFEXEC",
|
||||
],
|
||||
// flagWords - list of words to be always considered incorrect
|
||||
"flagWords": [
|
||||
|
||||
6
.devcontainer/Dockerfile
Normal file
6
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/rust:1-bullseye
|
||||
|
||||
# Install clang
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y clang \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -1,6 +1,25 @@
|
||||
{
|
||||
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/rust:1": {}
|
||||
}
|
||||
"name": "Rust",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"lldb.executable": "/usr/bin/lldb",
|
||||
// VS Code don't watch files under ./target
|
||||
"files.watcherExclude": {
|
||||
"**/target/**": true
|
||||
},
|
||||
"extensions": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"tamasfe.even-better-toml",
|
||||
"vadimcn.vscode-lldb",
|
||||
"mutantdino.resourcemonitor"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
|
||||
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1,8 +1,7 @@
|
||||
Lib/** linguist-vendored
|
||||
Cargo.lock linguist-generated -merge
|
||||
Cargo.lock linguist-generated
|
||||
*.snap linguist-generated -merge
|
||||
ast/src/ast_gen.rs linguist-generated -merge
|
||||
vm/src/stdlib/ast/gen.rs linguist-generated -merge
|
||||
compiler/parser/python.lalrpop text eol=LF
|
||||
Lib/*.py text working-tree-encoding=UTF-8 eol=LF
|
||||
**/*.rs text working-tree-encoding=UTF-8 eol=LF
|
||||
*.pck binary
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/empty.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/empty.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Generic issue template
|
||||
about: which is not covered by other templates
|
||||
title: ''
|
||||
labels:
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Short description of the issue. -->
|
||||
|
||||
## Details
|
||||
|
||||
<!-- Whatever you want to share -->
|
||||
16
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a feature to use RustPython (as a Rust library)
|
||||
title: ''
|
||||
labels: C-enhancement
|
||||
assignees: 'youknowone'
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Short description of the request. Please use incompatibility form to report missing features as Python interpreter -->
|
||||
|
||||
## Expected use case
|
||||
|
||||
<!-- By sharing detailed use case, we can understand the requirements better! If it will be used by open source projects, please also share the project URL. -->
|
||||
24
.github/ISSUE_TEMPLATE/report-bug.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/report-bug.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Report bugs
|
||||
about: Report a bug not related to CPython compatibility
|
||||
title: ''
|
||||
labels: C-bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Short description of the bug -->
|
||||
|
||||
## Expected
|
||||
|
||||
<!-- What's the expected result? Using ``` ``` block is preferred for text. -->
|
||||
|
||||
## Actual
|
||||
|
||||
<!-- What's the actual result? Using ``` ``` block is preferred for text. -->
|
||||
|
||||
## Python Documentation
|
||||
|
||||
<!-- If applicable. -->
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Report incompatibility
|
||||
about: Report an incompatibility between RustPython and CPython
|
||||
title: ''
|
||||
labels: feat
|
||||
labels: C-compat
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -11,6 +11,6 @@ assignees: ''
|
||||
|
||||
<!-- What Python feature is missing from RustPython? Give a short description of the feature and how you ran into its absence. -->
|
||||
|
||||
## Python Documentation
|
||||
## Python Documentation or reference to CPython source code
|
||||
|
||||
<!-- Give a link to the feature in the CPython documentation (https://docs.python.org/3/) in order to assist in its implementation. -->
|
||||
|
||||
186
.github/copilot-instructions.md
vendored
Normal file
186
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
# GitHub Copilot Instructions for RustPython
|
||||
|
||||
This document provides guidelines for working with GitHub Copilot when contributing to the RustPython project.
|
||||
|
||||
## Project Overview
|
||||
|
||||
RustPython is a Python 3 interpreter written in Rust, implementing Python 3.13.0+ compatibility. The project aims to provide:
|
||||
|
||||
- A complete Python-3 environment entirely in Rust (not CPython bindings)
|
||||
- A clean implementation without compatibility hacks
|
||||
- Cross-platform support, including WebAssembly compilation
|
||||
- The ability to embed Python scripting in Rust applications
|
||||
|
||||
## Repository Structure
|
||||
|
||||
- `src/` - Top-level code for the RustPython binary
|
||||
- `vm/` - The Python virtual machine implementation
|
||||
- `builtins/` - Python built-in types and functions
|
||||
- `stdlib/` - Essential standard library modules implemented in Rust, required to run the Python core
|
||||
- `compiler/` - Python compiler components
|
||||
- `parser/` - Parser for converting Python source to AST
|
||||
- `core/` - Bytecode representation in Rust structures
|
||||
- `codegen/` - AST to bytecode compiler
|
||||
- `Lib/` - CPython's standard library in Python (copied from CPython)
|
||||
- `derive/` - Rust macros for RustPython
|
||||
- `common/` - Common utilities
|
||||
- `extra_tests/` - Integration tests and snippets
|
||||
- `stdlib/` - Non-essential Python standard library modules implemented in Rust (useful but not required for core functionality)
|
||||
- `wasm/` - WebAssembly support
|
||||
- `jit/` - Experimental JIT compiler implementation
|
||||
- `pylib/` - Python standard library packaging (do not modify this directory directly - its contents are generated automatically)
|
||||
|
||||
## Important Development Notes
|
||||
|
||||
### Running Python Code
|
||||
|
||||
When testing Python code, always use RustPython instead of the standard `python` command:
|
||||
|
||||
```bash
|
||||
# Use this instead of python script.py
|
||||
cargo run -- script.py
|
||||
|
||||
# For interactive REPL
|
||||
cargo run
|
||||
|
||||
# With specific features
|
||||
cargo run --features ssl
|
||||
|
||||
# Release mode (recommended for better performance)
|
||||
cargo run --release -- script.py
|
||||
```
|
||||
|
||||
### Comparing with CPython
|
||||
|
||||
When you need to compare behavior with CPython or run test suites:
|
||||
|
||||
```bash
|
||||
# Use python command to explicitly run CPython
|
||||
python my_test_script.py
|
||||
|
||||
# Run RustPython
|
||||
cargo run -- my_test_script.py
|
||||
```
|
||||
|
||||
### Working with the Lib Directory
|
||||
|
||||
The `Lib/` directory contains Python standard library files copied from the CPython repository. Important notes:
|
||||
|
||||
- These files should be edited very conservatively
|
||||
- Modifications should be minimal and only to work around RustPython limitations
|
||||
- Tests in `Lib/test` often use one of the following markers:
|
||||
- Add a `# TODO: RUSTPYTHON` comment when modifications are made
|
||||
- `unittest.skip("TODO: RustPython <reason>")`
|
||||
- `unittest.expectedFailure` with `# TODO: RUSTPYTHON <reason>` comment
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run Rust unit tests
|
||||
cargo test --workspace --exclude rustpython_wasm
|
||||
|
||||
# Run Python snippets tests
|
||||
cd extra_tests
|
||||
pytest -v
|
||||
|
||||
# Run the Python test module
|
||||
cargo run --release -- -m test
|
||||
```
|
||||
|
||||
### Determining What to Implement
|
||||
|
||||
Run `./whats_left.py` to get a list of unimplemented methods, which is helpful when looking for contribution opportunities.
|
||||
|
||||
## Coding Guidelines
|
||||
|
||||
### Rust Code
|
||||
|
||||
- Follow the default rustfmt code style (`cargo fmt` to format)
|
||||
- Use clippy to lint code (`cargo clippy`)
|
||||
- Follow Rust best practices for error handling and memory management
|
||||
- Use the macro system (`pyclass`, `pymodule`, `pyfunction`, etc.) when implementing Python functionality in Rust
|
||||
|
||||
### Python Code
|
||||
|
||||
- Follow PEP 8 style for custom Python code
|
||||
- Use ruff for linting Python code
|
||||
- Minimize modifications to CPython standard library files
|
||||
|
||||
## Integration Between Rust and Python
|
||||
|
||||
The project provides several mechanisms for integration:
|
||||
|
||||
- `pymodule` macro for creating Python modules in Rust
|
||||
- `pyclass` macro for implementing Python classes in Rust
|
||||
- `pyfunction` macro for exposing Rust functions to Python
|
||||
- `PyObjectRef` and other types for working with Python objects in Rust
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Implementing a Python Module in Rust
|
||||
|
||||
```rust
|
||||
#[pymodule]
|
||||
mod mymodule {
|
||||
use rustpython_vm::prelude::*;
|
||||
|
||||
#[pyfunction]
|
||||
fn my_function(value: i32) -> i32 {
|
||||
value * 2
|
||||
}
|
||||
|
||||
#[pyattr]
|
||||
#[pyclass(name = "MyClass")]
|
||||
#[derive(Debug, PyPayload)]
|
||||
struct MyClass {
|
||||
value: usize,
|
||||
}
|
||||
|
||||
#[pyclass]
|
||||
impl MyClass {
|
||||
#[pymethod]
|
||||
fn get_value(&self) -> usize {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a Python Module to the Interpreter
|
||||
|
||||
```rust
|
||||
vm.add_native_module(
|
||||
"my_module_name".to_owned(),
|
||||
Box::new(my_module::make_module),
|
||||
);
|
||||
```
|
||||
|
||||
## Building for Different Targets
|
||||
|
||||
### WebAssembly
|
||||
|
||||
```bash
|
||||
# Build for WASM
|
||||
cargo build --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib --release
|
||||
```
|
||||
|
||||
### JIT Support
|
||||
|
||||
```bash
|
||||
# Enable JIT support
|
||||
cargo run --features jit
|
||||
```
|
||||
|
||||
### SSL Support
|
||||
|
||||
```bash
|
||||
# Enable SSL support
|
||||
cargo run --features ssl
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- Check the [architecture document](architecture/architecture.md) for a high-level overview
|
||||
- Read the [development guide](DEVELOPMENT.md) for detailed setup instructions
|
||||
- Generate documentation with `cargo doc --no-deps --all`
|
||||
- Online documentation is available at [docs.rs/rustpython](https://docs.rs/rustpython/)
|
||||
13
.github/dependabot.yml
vendored
Normal file
13
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*" # Group all Actions updates into a single larger pull request
|
||||
schedule:
|
||||
interval: weekly
|
||||
304
.github/workflows/ci.yaml
vendored
304
.github/workflows/ci.yaml
vendored
@@ -2,7 +2,9 @@ on:
|
||||
push:
|
||||
branches: [main, release]
|
||||
pull_request:
|
||||
types: [labeled, unlabeled, opened, synchronize, reopened]
|
||||
types: [unlabeled, opened, synchronize, reopened]
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
|
||||
name: CI
|
||||
|
||||
@@ -14,20 +16,31 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,ssl,jit
|
||||
NON_WASM_PACKAGES: >-
|
||||
-p rustpython-common
|
||||
-p rustpython-compiler-core
|
||||
-p rustpython-compiler
|
||||
-p rustpython-codegen
|
||||
-p rustpython-parser
|
||||
-p rustpython-vm
|
||||
-p rustpython-stdlib
|
||||
-p rustpython-jit
|
||||
-p rustpython-derive
|
||||
-p rustpython
|
||||
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl
|
||||
# Skip additional tests on Windows. They are checked on Linux and MacOS.
|
||||
# test_glob: many failing tests
|
||||
# test_io: many failing tests
|
||||
# test_os: many failing tests
|
||||
# test_pathlib: support.rmtree() failing
|
||||
# test_posixpath: OSError: (22, 'The filename, directory name, or volume label syntax is incorrect. (os error 123)')
|
||||
# test_venv: couple of failing tests
|
||||
WINDOWS_SKIPS: >-
|
||||
test_glob
|
||||
test_io
|
||||
test_os
|
||||
test_rlcompleter
|
||||
test_pathlib
|
||||
test_posixpath
|
||||
test_venv
|
||||
# configparser: https://github.com/RustPython/RustPython/issues/4995#issuecomment-1582397417
|
||||
# socketserver: seems related to configparser crash.
|
||||
MACOS_SKIPS: >-
|
||||
test_configparser
|
||||
test_socketserver
|
||||
# PLATFORM_INDEPENDENT_TESTS are tests that do not depend on the underlying OS. They are currently
|
||||
# only run on Linux to speed up the CI.
|
||||
PLATFORM_INDEPENDENT_TESTS: >-
|
||||
test_argparse
|
||||
test__colorize
|
||||
test_array
|
||||
test_asyncgen
|
||||
test_binop
|
||||
@@ -52,7 +65,6 @@ env:
|
||||
test_dis
|
||||
test_enumerate
|
||||
test_exception_variations
|
||||
test_exceptions
|
||||
test_float
|
||||
test_format
|
||||
test_fractions
|
||||
@@ -93,10 +105,11 @@ env:
|
||||
test_tuple
|
||||
test_types
|
||||
test_unary
|
||||
test_unicode
|
||||
test_unpack
|
||||
test_weakref
|
||||
test_yield_from
|
||||
# Python version targeted by the CI.
|
||||
PYTHON_VERSION: "3.13.1"
|
||||
|
||||
jobs:
|
||||
rust_tests:
|
||||
@@ -104,41 +117,48 @@ jobs:
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
name: Run rust tests
|
||||
needs: lalrpop
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Set up the Windows environment
|
||||
shell: bash
|
||||
run: |
|
||||
choco install llvm openssl
|
||||
echo "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" >>$GITHUB_ENV
|
||||
git config --system core.longpaths true
|
||||
cargo install --target-dir=target -v cargo-vcpkg
|
||||
cargo vcpkg -v build
|
||||
if: runner.os == 'Windows'
|
||||
- name: Set up the Mac environment
|
||||
run: brew install autoconf automake libtool
|
||||
if: runner.os == 'macOS'
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- name: run clippy
|
||||
run: cargo clippy ${{ env.CARGO_ARGS }} ${{ env.NON_WASM_PACKAGES }} -- -Dwarnings
|
||||
run: cargo clippy ${{ env.CARGO_ARGS }} --workspace --all-targets --exclude rustpython_wasm -- -Dwarnings
|
||||
|
||||
- name: run rust tests
|
||||
run: cargo test --workspace --exclude rustpython_wasm --verbose --features threading ${{ env.CARGO_ARGS }} ${{ env.NON_WASM_PACKAGES }}
|
||||
run: cargo test --workspace --exclude rustpython_wasm --verbose --features threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os != 'macOS'
|
||||
- name: run rust tests
|
||||
run: cargo test --workspace --exclude rustpython_wasm --exclude rustpython-jit --verbose --features threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: check compilation without threading
|
||||
run: cargo check ${{ env.CARGO_ARGS }}
|
||||
|
||||
- name: Test example projects
|
||||
run:
|
||||
cargo run --manifest-path example_projects/barebone/Cargo.toml
|
||||
cargo run --manifest-path example_projects/frozen_stdlib/Cargo.toml
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: prepare AppleSilicon build
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -159,16 +179,9 @@ jobs:
|
||||
exotic_targets:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Ensure compilation on various targets
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: i686-unknown-linux-gnu
|
||||
@@ -185,6 +198,15 @@ jobs:
|
||||
- name: Check compilation for android
|
||||
run: cargo check --target aarch64-linux-android
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Install gcc-aarch64-linux-gnu
|
||||
run: sudo apt install gcc-aarch64-linux-gnu
|
||||
- name: Check compilation for aarch64 linux gnu
|
||||
run: cargo check --target aarch64-unknown-linux-gnu
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: i686-unknown-linux-musl
|
||||
@@ -199,13 +221,6 @@ jobs:
|
||||
- name: Check compilation for freebsd
|
||||
run: cargo check --target x86_64-unknown-freebsd
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: Check compilation for wasm32
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: x86_64-unknown-freebsd
|
||||
@@ -216,14 +231,13 @@ jobs:
|
||||
- name: Prepare repository for redox compilation
|
||||
run: bash scripts/redox/uncomment-cargo.sh
|
||||
- name: Check compilation for Redox
|
||||
if: false # FIXME: redoxer toolchain is from ~july 2021, edition2021 isn't stabilized
|
||||
uses: coolreader18/redoxer-action@v1
|
||||
with:
|
||||
command: check
|
||||
args: --ignore-rust-version
|
||||
|
||||
snippets_cpython:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
needs: lalrpop
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
name: Run snippets and cpython tests
|
||||
@@ -233,32 +247,31 @@ jobs:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: Set up the Windows environment
|
||||
shell: bash
|
||||
run: |
|
||||
choco install llvm openssl
|
||||
echo "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" >>$GITHUB_ENV
|
||||
git config --system core.longpaths true
|
||||
cargo install cargo-vcpkg
|
||||
cargo vcpkg build
|
||||
if: runner.os == 'Windows'
|
||||
- name: Set up the Mac environment
|
||||
run: brew install autoconf automake libtool
|
||||
run: brew install autoconf automake libtool openssl@3
|
||||
if: runner.os == 'macOS'
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose --features=threading ${{ env.CARGO_ARGS }}
|
||||
- uses: actions/setup-python@v2
|
||||
if: runner.os == 'macOS'
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose --features=threading ${{ env.CARGO_ARGS }},jit
|
||||
if: runner.os != 'macOS'
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: run snippets
|
||||
run: python -m pip install -r requirements.txt && pytest -v
|
||||
working-directory: ./extra_tests
|
||||
@@ -266,113 +279,84 @@ jobs:
|
||||
name: run cpython platform-independent tests
|
||||
run:
|
||||
target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
- if: runner.os != 'Windows'
|
||||
name: run cpython platform-dependent tests
|
||||
- if: runner.os == 'Linux'
|
||||
name: run cpython platform-dependent tests (Linux)
|
||||
run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
- if: runner.os == 'macOS'
|
||||
name: run cpython platform-dependent tests (MacOS)
|
||||
run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.MACOS_SKIPS }}
|
||||
- if: runner.os == 'Windows'
|
||||
name: run cpython platform-dependent tests (windows partial - fixme)
|
||||
run:
|
||||
target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
test_glob
|
||||
test_importlib
|
||||
test_io
|
||||
test_iter
|
||||
test_os
|
||||
test_pathlib
|
||||
test_posixpath
|
||||
test_shutil
|
||||
test_venv
|
||||
target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.WINDOWS_SKIPS }}
|
||||
- if: runner.os != 'Windows'
|
||||
name: check that --install-pip succeeds
|
||||
run: |
|
||||
mkdir site-packages
|
||||
target/release/rustpython --install-pip ensurepip --user
|
||||
|
||||
lalrpop:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Generate parser with lalrpop
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- name: Check if cached generated parser exists
|
||||
id: generated_parser
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "compiler/parser/python.rs"
|
||||
- if: runner.os == 'Windows'
|
||||
name: Force python.lalrpop to be lf # actions@checkout ignore .gitattributes
|
||||
target/release/rustpython -m pip install six
|
||||
- if: runner.os != 'Windows'
|
||||
name: Check that ensurepip succeeds.
|
||||
run: |
|
||||
set file compiler/parser/python.lalrpop; ((Get-Content $file) -join "`n") + "`n" | Set-Content -NoNewline $file
|
||||
- name: Install lalrpop
|
||||
if: steps.generated_parser.outputs.files_exists == 'false'
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
with:
|
||||
crate: lalrpop
|
||||
version: "0.19.8"
|
||||
- name: Run lalrpop
|
||||
if: steps.generated_parser.outputs.files_exists == 'false'
|
||||
run: lalrpop compiler/parser/python.lalrpop
|
||||
target/release/rustpython -m ensurepip
|
||||
target/release/rustpython -c "import pip"
|
||||
- if: runner.os != 'Windows'
|
||||
name: Check if pip inside venv is functional
|
||||
run: |
|
||||
target/release/rustpython -m venv testvenv
|
||||
testvenv/bin/rustpython -m pip install wheel
|
||||
- name: Check whats_left is not broken
|
||||
run: python -I whats_left.py
|
||||
|
||||
lint:
|
||||
name: Check Rust code with rustfmt and clippy
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- name: run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
run: cargo fmt --check
|
||||
- name: run clippy on wasm
|
||||
run: cargo clippy --manifest-path=wasm/lib/Cargo.toml -- -Dwarnings
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: install flake8
|
||||
run: python -m pip install flake8
|
||||
- name: run lint
|
||||
run: flake8 . --count --exclude=./.*,./Lib,./vm/Lib,./benches/ --select=E9,F63,F7,F82 --show-source --statistics
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: install ruff
|
||||
run: python -m pip install ruff==0.11.8
|
||||
- name: Ensure docs generate no warnings
|
||||
run: cargo doc
|
||||
- name: run ruff check
|
||||
run: ruff check --diff
|
||||
- name: run ruff format
|
||||
run: ruff format --check
|
||||
- name: install prettier
|
||||
run: yarn global add prettier && echo "$(yarn global bin)" >>$GITHUB_PATH
|
||||
- name: check wasm code with prettier
|
||||
# prettier doesn't handle ignore files very well: https://github.com/prettier/prettier/issues/8506
|
||||
run: cd wasm && git ls-files -z | xargs -0 prettier --check -u
|
||||
- name: Check update_asdl.sh consistency
|
||||
run: bash scripts/update_asdl.sh && git diff --exit-code
|
||||
- name: Check whats_left is not broken
|
||||
run: python -I whats_left.py
|
||||
# Keep cspell check as the last step. This is optional test.
|
||||
- name: install extra dictionaries
|
||||
run: npm install @cspell/dict-en_us @cspell/dict-cpp @cspell/dict-python @cspell/dict-rust @cspell/dict-win32 @cspell/dict-shell
|
||||
- name: spell checker
|
||||
uses: streetsidesoftware/cspell-action@v7
|
||||
with:
|
||||
files: '**/*.rs'
|
||||
incremental_files_only: true
|
||||
|
||||
miri:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Run tests under miri
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly
|
||||
components: miri
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run tests under miri
|
||||
# miri-ignore-leaks because the type-object circular reference means that there will always be
|
||||
# a memory leak, at least until we have proper cyclic gc
|
||||
@@ -381,46 +365,57 @@ jobs:
|
||||
wasm:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Check the WASM package and demo
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- name: install geckodriver
|
||||
run: |
|
||||
wget https://github.com/mozilla/geckodriver/releases/download/v0.30.0/geckodriver-v0.30.0-linux64.tar.gz
|
||||
wget https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz
|
||||
mkdir geckodriver
|
||||
tar -xzf geckodriver-v0.30.0-linux64.tar.gz -C geckodriver
|
||||
- uses: actions/setup-python@v2
|
||||
tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- run: python -m pip install -r requirements.txt
|
||||
working-directory: ./wasm/tests
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "wasm/demo/package-lock.json"
|
||||
- name: run test
|
||||
run: |
|
||||
export PATH=$PATH:`pwd`/../../geckodriver
|
||||
npm install
|
||||
npm run test
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/demo
|
||||
- uses: mwilliamson/setup-wabt-action@v3
|
||||
with: { wabt-version: "1.0.36" }
|
||||
- name: check wasm32-unknown without js
|
||||
run: |
|
||||
cd wasm/wasm-unknown-test
|
||||
cargo build --release --verbose
|
||||
if wasm-objdump -xj Import target/wasm32-unknown-unknown/release/wasm_unknown_test.wasm; then
|
||||
echo "ERROR: wasm32-unknown module expects imports from the host environment" >2
|
||||
fi
|
||||
- name: build notebook demo
|
||||
if: github.ref == 'refs/heads/release'
|
||||
run: |
|
||||
npm install
|
||||
npm run dist
|
||||
mv dist ../demo/dist/notebook
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/notebook
|
||||
- name: Deploy demo to Github Pages
|
||||
if: success() && github.ref == 'refs/heads/release'
|
||||
uses: peaceiris/actions-gh-pages@v2
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
env:
|
||||
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEMO_DEPLOY_KEY }}
|
||||
PUBLISH_DIR: ./wasm/demo/dist
|
||||
@@ -430,24 +425,21 @@ jobs:
|
||||
wasm-wasi:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Run snippets and cpython tests on wasm-wasi
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-wasi
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
target: wasm32-wasip1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Setup Wasmer
|
||||
uses: wasmerio/setup-wasmer@v1
|
||||
uses: wasmerio/setup-wasmer@v3
|
||||
- name: Install clang
|
||||
run: sudo apt-get update && sudo apt-get install clang -y
|
||||
- name: build rustpython
|
||||
run: cargo build --release --target wasm32-wasi --features freeze-stdlib,stdlib --verbose
|
||||
run: cargo build --release --target wasm32-wasip1 --features freeze-stdlib,stdlib --verbose
|
||||
- name: run snippets
|
||||
run: wasmer run --dir . target/wasm32-wasi/release/rustpython.wasm -- extra_tests/snippets/stdlib_random.py
|
||||
run: wasmer run --dir `pwd` target/wasm32-wasip1/release/rustpython.wasm -- `pwd`/extra_tests/snippets/stdlib_random.py
|
||||
- name: run cpython unittest
|
||||
run: wasmer run --dir `pwd` target/wasm32-wasip1/release/rustpython.wasm -- `pwd`/Lib/test/test_int.py
|
||||
|
||||
127
.github/workflows/cron-ci.yaml
vendored
127
.github/workflows/cron-ci.yaml
vendored
@@ -2,75 +2,50 @@ on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 6'
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/cron-ci.yaml
|
||||
|
||||
name: Periodic checks/tasks
|
||||
|
||||
env:
|
||||
CARGO_ARGS: --features ssl,jit
|
||||
CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,ssl,jit
|
||||
PYTHON_VERSION: "3.13.1"
|
||||
|
||||
jobs:
|
||||
# codecov collects code coverage data from the rust tests, python snippets and python test suite.
|
||||
# This is done using cargo-llvm-cov, which is a wrapper around llvm-cov.
|
||||
codecov:
|
||||
name: Collect code coverage data
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- run: sudo apt-get update && sudo apt-get -y install lcov
|
||||
- run: cargo build --release --verbose ${{ env.CARGO_ARGS }}
|
||||
env:
|
||||
RUSTC_WRAPPER: './scripts/codecoverage-rustc-wrapper.sh'
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- run: python -m pip install pytest
|
||||
working-directory: ./extra_tests
|
||||
- name: run snippets
|
||||
run: LLVM_PROFILE_FILE="$PWD/snippet-%p.profraw" pytest -v
|
||||
working-directory: ./extra_tests
|
||||
- name: Run cargo-llvm-cov with Rust tests.
|
||||
run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --verbose --no-default-features --features stdlib,importlib,encodings,ssl,jit
|
||||
- name: Run cargo-llvm-cov with Python snippets.
|
||||
run: python scripts/cargo-llvm-cov.py
|
||||
continue-on-error: true
|
||||
- name: run cpython tests
|
||||
run: |
|
||||
alltests=($(target/release/rustpython -c 'from test.libregrtest.runtest import findtests; print(*findtests())'))
|
||||
i=0
|
||||
# chunk into chunks of 10 tests each. idk at this point
|
||||
while subtests=("${alltests[@]:$i:10}"); [[ ${#subtests[@]} -ne 0 ]]; do
|
||||
LLVM_PROFILE_FILE="$PWD/regrtest-%p.profraw" target/release/rustpython -m test -v "${subtests[@]}" || true
|
||||
((i+=10))
|
||||
done
|
||||
- name: Run cargo-llvm-cov with Python test suite.
|
||||
run: cargo llvm-cov --no-report run -- -m test -u all --slowest --fail-env-changed
|
||||
continue-on-error: true
|
||||
- name: prepare code coverage data
|
||||
run: |
|
||||
rusttool() {
|
||||
local tool=$1; shift; "$(rustc --print target-libdir)/../bin/llvm-$tool" "$@"
|
||||
}
|
||||
rusttool profdata merge extra_tests/snippet-*.profraw regrtest-*.profraw --output codecov.profdata
|
||||
rusttool cov export --instr-profile codecov.profdata target/release/rustpython --format lcov > codecov_tmp.lcov
|
||||
lcov -e codecov_tmp.lcov "$PWD"/'*' -o codecov_tmp2.lcov
|
||||
lcov -r codecov_tmp2.lcov "$PWD"/target/'*' -o codecov.lcov # remove LALRPOP-generated parser
|
||||
- name: upload to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
- name: Prepare code coverage data
|
||||
run: cargo llvm-cov report --lcov --output-path='codecov.lcov'
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./codecov.lcov
|
||||
|
||||
testdata:
|
||||
name: Collect regression test data
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose
|
||||
@@ -97,22 +72,19 @@ jobs:
|
||||
|
||||
whatsleft:
|
||||
name: Collect what is left data
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose
|
||||
- name: Collect what is left data
|
||||
run: |
|
||||
chmod +x ./whats_left.py
|
||||
./whats_left.py > whats_left.temp
|
||||
./whats_left.py --features "ssl,sqlite" > whats_left.temp
|
||||
env:
|
||||
RUSTPYTHONPATH: ${{ github.workspace }}/Lib
|
||||
- name: Upload data to the website
|
||||
@@ -128,6 +100,9 @@ jobs:
|
||||
cd website
|
||||
[ -f ./_data/whats_left.temp ] && cp ./_data/whats_left.temp ./_data/whats_left_lastrun.temp
|
||||
cp ../whats_left.temp ./_data/whats_left.temp
|
||||
rm ./_data/whats_left/modules.csv
|
||||
echo -e "module" > ./_data/whats_left/modules.csv
|
||||
cat ./_data/whats_left.temp | grep "(entire module)" | cut -d ' ' -f 1 | sort >> ./_data/whats_left/modules.csv
|
||||
git add -A
|
||||
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update what is left results" --author="$GITHUB_ACTOR"; then
|
||||
git push
|
||||
@@ -135,17 +110,11 @@ jobs:
|
||||
|
||||
benchmark:
|
||||
name: Collect benchmark data
|
||||
needs: lalrpop
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: cargo install cargo-criterion
|
||||
@@ -183,35 +152,3 @@ jobs:
|
||||
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update benchmark results"; then
|
||||
git push
|
||||
fi
|
||||
|
||||
lalrpop:
|
||||
name: Generate parser with lalrpop
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache generated parser
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: compiler/parser/python.rs
|
||||
key: lalrpop-${{ hashFiles('compiler/parser/python.lalrpop') }}
|
||||
- name: Check if cached generated parser exists
|
||||
id: generated_parser
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "compiler/parser/python.rs"
|
||||
- if: runner.os == 'Windows'
|
||||
name: Force python.lalrpop to be lf # actions@checkout ignore .gitattributes
|
||||
run: |
|
||||
set file compiler/parser/python.lalrpop; ((Get-Content $file) -join "`n") + "`n" | Set-Content -NoNewline $file
|
||||
- name: Install lalrpop
|
||||
if: steps.generated_parser.outputs.files_exists == 'false'
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
with:
|
||||
crate: lalrpop
|
||||
version: "0.19.8"
|
||||
- name: Run lalrpop
|
||||
if: steps.generated_parser.outputs.files_exists == 'false'
|
||||
run: lalrpop compiler/parser/python.lalrpop
|
||||
|
||||
173
.github/workflows/release.yml
vendored
Normal file
173
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,173 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 9 AM UTC on every Monday
|
||||
- cron: "0 9 * * Mon"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pre-release:
|
||||
type: boolean
|
||||
description: Mark "Pre-Release"
|
||||
required: false
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
CARGO_ARGS: --no-default-features --features stdlib,importlib,encodings,sqlite,ssl
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.platform.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- runner: ubuntu-latest
|
||||
target: x86_64-unknown-linux-gnu
|
||||
# - runner: ubuntu-latest
|
||||
# target: i686-unknown-linux-gnu
|
||||
# - runner: ubuntu-latest
|
||||
# target: aarch64-unknown-linux-gnu
|
||||
# - runner: ubuntu-latest
|
||||
# target: armv7-unknown-linux-gnueabi
|
||||
# - runner: ubuntu-latest
|
||||
# target: s390x-unknown-linux-gnu
|
||||
# - runner: ubuntu-latest
|
||||
# target: powerpc64le-unknown-linux-gnu
|
||||
- runner: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
# - runner: macos-latest
|
||||
# target: x86_64-apple-darwin
|
||||
- runner: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
# - runner: windows-latest
|
||||
# target: i686-pc-windows-msvc
|
||||
# - runner: windows-latest
|
||||
# target: aarch64-pc-windows-msvc
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Set up Environment
|
||||
shell: bash
|
||||
run: rustup target add ${{ matrix.platform.target }}
|
||||
- name: Set up Windows Environment
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
cargo install --target-dir=target -v cargo-vcpkg
|
||||
cargo vcpkg -v build
|
||||
if: runner.os == 'Windows'
|
||||
- name: Set up MacOS Environment
|
||||
run: brew install autoconf automake libtool
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: Build RustPython
|
||||
run: cargo build --release --target=${{ matrix.platform.target }} --verbose --features=threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os == 'macOS'
|
||||
- name: Build RustPython
|
||||
run: cargo build --release --target=${{ matrix.platform.target }} --verbose --features=threading ${{ env.CARGO_ARGS }},jit
|
||||
if: runner.os != 'macOS'
|
||||
|
||||
- name: Rename Binary
|
||||
run: cp target/${{ matrix.platform.target }}/release/rustpython target/rustpython-release-${{ runner.os }}-${{ matrix.platform.target }}
|
||||
if: runner.os != 'Windows'
|
||||
- name: Rename Binary
|
||||
run: cp target/${{ matrix.platform.target }}/release/rustpython.exe target/rustpython-release-${{ runner.os }}-${{ matrix.platform.target }}.exe
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
- name: Upload Binary Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rustpython-release-${{ runner.os }}-${{ matrix.platform.target }}
|
||||
path: target/rustpython-release-${{ runner.os }}-${{ matrix.platform.target }}*
|
||||
|
||||
build-wasm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-wasip1
|
||||
|
||||
- name: Build RustPython
|
||||
run: cargo build --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib --release
|
||||
|
||||
- name: Rename Binary
|
||||
run: cp target/wasm32-wasip1/release/rustpython.wasm target/rustpython-release-wasm32-wasip1.wasm
|
||||
|
||||
- name: Upload Binary Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: rustpython-release-wasm32-wasip1
|
||||
path: target/rustpython-release-wasm32-wasip1.wasm
|
||||
|
||||
- name: install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: mwilliamson/setup-wabt-action@v3
|
||||
with: { wabt-version: "1.0.30" }
|
||||
- name: build demo
|
||||
run: |
|
||||
npm install
|
||||
npm run dist
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/demo
|
||||
- name: build notebook demo
|
||||
run: |
|
||||
npm install
|
||||
npm run dist
|
||||
mv dist ../demo/dist/notebook
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/notebook
|
||||
- name: Deploy demo to Github Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
deploy_key: ${{ secrets.ACTIONS_DEMO_DEPLOY_KEY }}
|
||||
publish_dir: ./wasm/demo/dist
|
||||
external_repository: RustPython/demo
|
||||
publish_branch: master
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, build-wasm]
|
||||
steps:
|
||||
- name: Download Binary Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: bin
|
||||
pattern: rustpython-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: List Binaries
|
||||
run: |
|
||||
ls -lah bin/
|
||||
file bin/*
|
||||
- name: Create Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
run: ${{ github.run_number }}
|
||||
run: |
|
||||
if [[ "${{ github.event.inputs.pre-release }}" == "false" ]]; then
|
||||
RELEASE_TYPE_NAME=Release
|
||||
PRERELEASE_ARG=
|
||||
else
|
||||
RELEASE_TYPE_NAME=Pre-Release
|
||||
PRERELEASE_ARG=--prerelease
|
||||
fi
|
||||
|
||||
today=$(date '+%Y-%m-%d')
|
||||
gh release create "$today-$tag-$run" \
|
||||
--repo="$GITHUB_REPOSITORY" \
|
||||
--title="RustPython $RELEASE_TYPE_NAME $today-$tag #$run" \
|
||||
--target="$tag" \
|
||||
--generate-notes \
|
||||
$PRERELEASE_ARG \
|
||||
bin/rustpython-release-*
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -2,13 +2,15 @@
|
||||
/*/target
|
||||
**/*.rs.bk
|
||||
**/*.bytecode
|
||||
__pycache__
|
||||
__pycache__/
|
||||
**/*.pytest_cache
|
||||
.*sw*
|
||||
.repl_history.txt
|
||||
.vscode
|
||||
.vscode/
|
||||
wasm-pack.log
|
||||
.idea/
|
||||
.envrc
|
||||
.python-version
|
||||
|
||||
flame-graph.html
|
||||
flame.txt
|
||||
@@ -19,5 +21,3 @@ flamescope.json
|
||||
|
||||
extra_tests/snippets/resources
|
||||
extra_tests/not_impl.py
|
||||
|
||||
compiler/parser/python.rs
|
||||
|
||||
21
.vscode/launch.json
vendored
21
.vscode/launch.json
vendored
@@ -8,15 +8,24 @@
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'rustpython'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--package=rustpython"
|
||||
],
|
||||
},
|
||||
"preLaunchTask": "Build RustPython Debug",
|
||||
"program": "target/debug/rustpython",
|
||||
"args": [],
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'rustpython' without SSL",
|
||||
"preLaunchTask": "Build RustPython Debug without SSL",
|
||||
"program": "target/debug/rustpython",
|
||||
"args": [],
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
|
||||
20
.vscode/tasks.json
vendored
20
.vscode/tasks.json
vendored
@@ -2,7 +2,7 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build RustPython Debug",
|
||||
"label": "Build RustPython Debug without SSL",
|
||||
"type": "shell",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
@@ -15,6 +15,22 @@
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Build RustPython Debug",
|
||||
"type": "shell",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"--features=ssl"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$rustc",
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
2893
Cargo.lock
generated
2893
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
216
Cargo.toml
216
Cargo.toml
@@ -1,62 +1,54 @@
|
||||
# REDOX START
|
||||
# cargo-features = ["edition2021"]
|
||||
# REDOX END
|
||||
[package]
|
||||
name = "rustpython"
|
||||
version = "0.2.0"
|
||||
authors = ["RustPython Team"]
|
||||
edition = "2021"
|
||||
description = "A python interpreter written in rust."
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
include = ["LICENSE", "Cargo.toml", "src/**/*.rs"]
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"compiler", "compiler/ast", "compiler/core", "compiler/codegen", "compiler/parser",
|
||||
".", "common", "derive", "jit", "vm", "pylib", "stdlib", "wasm/lib", "derive-impl",
|
||||
]
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["threading", "stdlib", "zlib", "importlib", "encodings", "rustpython-parser/lalrpop"]
|
||||
default = ["threading", "stdlib", "stdio", "importlib"]
|
||||
importlib = ["rustpython-vm/importlib"]
|
||||
encodings = ["rustpython-vm/encodings"]
|
||||
stdlib = ["rustpython-stdlib", "rustpython-pylib"]
|
||||
stdio = ["rustpython-vm/stdio"]
|
||||
stdlib = ["rustpython-stdlib", "rustpython-pylib", "encodings"]
|
||||
flame-it = ["rustpython-vm/flame-it", "flame", "flamescope"]
|
||||
freeze-stdlib = ["rustpython-vm/freeze-stdlib", "rustpython-pylib?/freeze-stdlib"]
|
||||
freeze-stdlib = ["stdlib", "rustpython-vm/freeze-stdlib", "rustpython-pylib?/freeze-stdlib"]
|
||||
jit = ["rustpython-vm/jit"]
|
||||
threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"]
|
||||
zlib = ["stdlib", "rustpython-stdlib/zlib"]
|
||||
bz2 = ["stdlib", "rustpython-stdlib/bz2"]
|
||||
sqlite = ["rustpython-stdlib/sqlite"]
|
||||
ssl = ["rustpython-stdlib/ssl"]
|
||||
ssl-vendor = ["rustpython-stdlib/ssl-vendor"]
|
||||
ssl-vendor = ["ssl", "rustpython-stdlib/ssl-vendor"]
|
||||
tkinter = ["rustpython-stdlib/tkinter"]
|
||||
|
||||
[dependencies]
|
||||
rustpython-compiler = { path = "compiler", version = "0.2.0" }
|
||||
rustpython-parser = { path = "compiler/parser", version = "0.2.0" }
|
||||
rustpython-pylib = { path = "pylib", optional = true, default-features = false }
|
||||
rustpython-stdlib = { path = "stdlib", optional = true, default-features = false }
|
||||
rustpython-vm = { path = "vm", version = "0.2.0", default-features = false, features = ["compiler"] }
|
||||
rustpython-compiler = { workspace = true }
|
||||
rustpython-pylib = { workspace = true, optional = true }
|
||||
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
|
||||
rustpython-vm = { workspace = true, features = ["compiler"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
cfg-if = "1.0.0"
|
||||
clap = "2.34"
|
||||
dirs = { package = "dirs-next", version = "2.0.0" }
|
||||
env_logger = { version = "0.9.0", default-features = false, features = ["atty", "termcolor"] }
|
||||
flame = { version = "0.2.2", optional = true }
|
||||
cfg-if = { workspace = true }
|
||||
log = { workspace = true }
|
||||
flame = { workspace = true, optional = true }
|
||||
|
||||
lexopt = "0.3"
|
||||
dirs = { package = "dirs-next", version = "2.0" }
|
||||
env_logger = "0.11"
|
||||
flamescope = { version = "0.1.2", optional = true }
|
||||
libc = "0.2.133"
|
||||
log = "0.4.16"
|
||||
num-traits = "0.2.14"
|
||||
atty = "0.2.14"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rustyline = "10.0.0"
|
||||
rustyline = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cpython = "0.7.0"
|
||||
criterion = "0.3.5"
|
||||
python3-sys = "0.7.0"
|
||||
criterion = { workspace = true }
|
||||
pyo3 = { version = "0.24", features = ["auto-initialize"] }
|
||||
|
||||
[[bench]]
|
||||
name = "execution"
|
||||
@@ -87,5 +79,149 @@ opt-level = 3
|
||||
lto = "thin"
|
||||
|
||||
[patch.crates-io]
|
||||
# REDOX START, Uncommment when you want to compile/check with redoxer
|
||||
radium = { version = "1.1.0", git = "https://github.com/youknowone/ferrilab", branch = "fix-nightly" }
|
||||
# REDOX START, Uncomment when you want to compile/check with redoxer
|
||||
# REDOX END
|
||||
|
||||
# Used only on Windows to build the vcpkg dependencies
|
||||
[package.metadata.vcpkg]
|
||||
git = "https://github.com/microsoft/vcpkg"
|
||||
# The revision of the vcpkg repository to use
|
||||
# https://github.com/microsoft/vcpkg/tags
|
||||
rev = "2024.02.14"
|
||||
|
||||
[package.metadata.vcpkg.target]
|
||||
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md", dev-dependencies = ["openssl" ] }
|
||||
|
||||
[package.metadata.packager]
|
||||
product-name = "RustPython"
|
||||
identifier = "com.rustpython.rustpython"
|
||||
description = "An open source Python 3 interpreter written in Rust"
|
||||
homepage = "https://rustpython.github.io/"
|
||||
license_file = "LICENSE"
|
||||
authors = ["RustPython Team"]
|
||||
publisher = "RustPython Team"
|
||||
resources = ["LICENSE", "README.md", "Lib"]
|
||||
icons = ["32x32.png"]
|
||||
|
||||
[package.metadata.packager.nsis]
|
||||
installer_mode = "both"
|
||||
template = "installer-config/installer.nsi"
|
||||
|
||||
[package.metadata.packager.wix]
|
||||
template = "installer-config/installer.wxs"
|
||||
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"compiler", "compiler/core", "compiler/codegen", "compiler/literal", "compiler/source",
|
||||
".", "common", "derive", "jit", "vm", "vm/sre_engine", "pylib", "stdlib", "derive-impl", "wtf8",
|
||||
"wasm/lib",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.0"
|
||||
authors = ["RustPython Team"]
|
||||
edition = "2024"
|
||||
rust-version = "1.85.0"
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
rustpython-compiler-source = { path = "compiler/source" }
|
||||
rustpython-compiler-core = { path = "compiler/core", version = "0.4.0" }
|
||||
rustpython-compiler = { path = "compiler", version = "0.4.0" }
|
||||
rustpython-codegen = { path = "compiler/codegen", version = "0.4.0" }
|
||||
rustpython-common = { path = "common", version = "0.4.0" }
|
||||
rustpython-derive = { path = "derive", version = "0.4.0" }
|
||||
rustpython-derive-impl = { path = "derive-impl", version = "0.4.0" }
|
||||
rustpython-jit = { path = "jit", version = "0.4.0" }
|
||||
rustpython-literal = { path = "compiler/literal", version = "0.4.0" }
|
||||
rustpython-vm = { path = "vm", default-features = false, version = "0.4.0" }
|
||||
rustpython-pylib = { path = "pylib", version = "0.4.0" }
|
||||
rustpython-stdlib = { path = "stdlib", default-features = false, version = "0.4.0" }
|
||||
rustpython-sre_engine = { path = "vm/sre_engine", version = "0.4.0" }
|
||||
rustpython-wtf8 = { path = "wtf8", version = "0.4.0" }
|
||||
rustpython-doc = { git = "https://github.com/RustPython/__doc__", tag = "0.3.0", version = "0.3.0" }
|
||||
|
||||
ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", tag = "0.11.0" }
|
||||
ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", tag = "0.11.0" }
|
||||
ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", tag = "0.11.0" }
|
||||
ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", tag = "0.11.0" }
|
||||
|
||||
ahash = "0.8.11"
|
||||
ascii = "1.1"
|
||||
bitflags = "2.4.2"
|
||||
bstr = "1"
|
||||
cfg-if = "1.0"
|
||||
chrono = "0.4.39"
|
||||
constant_time_eq = "0.4"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
crossbeam-utils = "0.8.21"
|
||||
flame = "0.2.2"
|
||||
getrandom = { version = "0.3", features = ["std"] }
|
||||
glob = "0.3"
|
||||
hex = "0.4.3"
|
||||
indexmap = { version = "2.2.6", features = ["std"] }
|
||||
insta = "1.42"
|
||||
itertools = "0.14.0"
|
||||
is-macro = "0.3.7"
|
||||
junction = "1.2.0"
|
||||
libc = "0.2.169"
|
||||
libffi = "4.0"
|
||||
log = "0.4.27"
|
||||
nix = { version = "0.29", features = ["fs", "user", "process", "term", "time", "signal", "ioctl", "socket", "sched", "zerocopy", "dir", "hostname", "net", "poll"] }
|
||||
malachite-bigint = "0.6"
|
||||
malachite-q = "0.6"
|
||||
malachite-base = "0.6"
|
||||
memchr = "2.7.4"
|
||||
num-complex = "0.4.6"
|
||||
num-integer = "0.1.46"
|
||||
num-traits = "0.2"
|
||||
num_enum = { version = "0.7", default-features = false }
|
||||
optional = "0.5"
|
||||
once_cell = "1.20.3"
|
||||
parking_lot = "0.12.3"
|
||||
paste = "1.0.15"
|
||||
proc-macro2 = "1.0.93"
|
||||
pymath = "0.0.2"
|
||||
quote = "1.0.38"
|
||||
radium = "1.1"
|
||||
rand = "0.9"
|
||||
rand_core = { version = "0.9", features = ["os_rng"] }
|
||||
rustix = { version = "1.0", features = ["event"] }
|
||||
rustyline = "15.0.0"
|
||||
serde = { version = "1.0.133", default-features = false }
|
||||
schannel = "0.1.27"
|
||||
static_assertions = "1.1"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
syn = "2"
|
||||
thiserror = "2.0"
|
||||
thread_local = "1.1.8"
|
||||
unicode-casing = "0.1.0"
|
||||
unic-char-property = "0.9.0"
|
||||
unic-normal = "0.9.0"
|
||||
unic-ucd-age = "0.9.0"
|
||||
unic-ucd-bidi = "0.9.0"
|
||||
unic-ucd-category = "0.9.0"
|
||||
unic-ucd-ident = "0.9.0"
|
||||
unicode_names2 = "1.3.0"
|
||||
widestring = "1.1.0"
|
||||
windows-sys = "0.59.0"
|
||||
wasm-bindgen = "0.2.100"
|
||||
|
||||
# Lints
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "allow"
|
||||
unsafe_op_in_unsafe_fn = "deny"
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
perf = "warn"
|
||||
style = "warn"
|
||||
complexity = "warn"
|
||||
suspicious = "warn"
|
||||
correctness = "warn"
|
||||
|
||||
@@ -19,13 +19,13 @@ The contents of the Development Guide include:
|
||||
|
||||
RustPython requires the following:
|
||||
|
||||
- Rust latest stable version (e.g 1.51.0 as of Apr 2 2021)
|
||||
- Rust latest stable version (e.g 1.69.0 as of Apr 20 2023)
|
||||
- To check Rust version: `rustc --version`
|
||||
- If you have `rustup` on your system, enter to update to the latest
|
||||
stable version: `rustup update stable`
|
||||
- If you do not have Rust installed, use [rustup](https://rustup.rs/) to
|
||||
do so.
|
||||
- CPython version 3.10 or higher
|
||||
- CPython version 3.13 or higher
|
||||
- CPython can be installed by your operating system's package manager,
|
||||
from the [Python website](https://www.python.org/downloads/), or
|
||||
using a third-party distribution, such as
|
||||
@@ -47,7 +47,10 @@ you can check yourself with `cargo clippy`.
|
||||
|
||||
Custom Python code (i.e. code not copied from CPython's standard library) should
|
||||
follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style. We also use
|
||||
[flake8](http://flake8.pycqa.org/en/latest/) to check Python code style.
|
||||
[ruff](https://beta.ruff.rs/docs/) to check Python code style.
|
||||
|
||||
In addition to language specific tools, [cspell](https://github.com/streetsidesoftware/cspell),
|
||||
a code spell checker, is used in order to ensure correct spellings for code.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 RustPython Team
|
||||
Copyright (c) 2025 RustPython Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
6
Lib/__future__.py
vendored
6
Lib/__future__.py
vendored
@@ -33,7 +33,7 @@ in releases at or after that, modules no longer need
|
||||
to use the feature in question, but may continue to use such imports.
|
||||
|
||||
MandatoryRelease may also be None, meaning that a planned feature got
|
||||
dropped.
|
||||
dropped or that the release version is undetermined.
|
||||
|
||||
Instances of class _Feature have two corresponding methods,
|
||||
.getOptionalRelease() and .getMandatoryRelease().
|
||||
@@ -96,7 +96,7 @@ class _Feature:
|
||||
"""Return release in which this feature will become mandatory.
|
||||
|
||||
This is a 5-tuple, of the same form as sys.version_info, or, if
|
||||
the feature was dropped, is None.
|
||||
the feature was dropped, or the release date is undetermined, is None.
|
||||
"""
|
||||
return self.mandatory
|
||||
|
||||
@@ -143,5 +143,5 @@ generator_stop = _Feature((3, 5, 0, "beta", 1),
|
||||
CO_FUTURE_GENERATOR_STOP)
|
||||
|
||||
annotations = _Feature((3, 7, 0, "beta", 1),
|
||||
(3, 11, 0, "alpha", 0),
|
||||
None,
|
||||
CO_FUTURE_ANNOTATIONS)
|
||||
|
||||
16
Lib/__hello__.py
vendored
Normal file
16
Lib/__hello__.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
initialized = True
|
||||
|
||||
class TestFrozenUtf8_1:
|
||||
"""\u00b6"""
|
||||
|
||||
class TestFrozenUtf8_2:
|
||||
"""\u03c0"""
|
||||
|
||||
class TestFrozenUtf8_4:
|
||||
"""\U0001f600"""
|
||||
|
||||
def main():
|
||||
print("Hello world!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
7
Lib/__phello__/__init__.py
vendored
Normal file
7
Lib/__phello__/__init__.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
initialized = True
|
||||
|
||||
def main():
|
||||
print("Hello world!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
7
Lib/__phello__/spam.py
vendored
Normal file
7
Lib/__phello__/spam.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
initialized = True
|
||||
|
||||
def main():
|
||||
print("Hello world!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
108
Lib/_aix_support.py
vendored
Normal file
108
Lib/_aix_support.py
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Shared AIX support functions."""
|
||||
|
||||
import sys
|
||||
import sysconfig
|
||||
|
||||
|
||||
# Taken from _osx_support _read_output function
|
||||
def _read_cmd_output(commandstring, capture_stderr=False):
|
||||
"""Output from successful command execution or None"""
|
||||
# Similar to os.popen(commandstring, "r").read(),
|
||||
# but without actually using os.popen because that
|
||||
# function is not usable during python bootstrap.
|
||||
import os
|
||||
import contextlib
|
||||
fp = open("/tmp/_aix_support.%s"%(
|
||||
os.getpid(),), "w+b")
|
||||
|
||||
with contextlib.closing(fp) as fp:
|
||||
if capture_stderr:
|
||||
cmd = "%s >'%s' 2>&1" % (commandstring, fp.name)
|
||||
else:
|
||||
cmd = "%s 2>/dev/null >'%s'" % (commandstring, fp.name)
|
||||
return fp.read() if not os.system(cmd) else None
|
||||
|
||||
|
||||
def _aix_tag(vrtl, bd):
|
||||
# type: (List[int], int) -> str
|
||||
# Infer the ABI bitwidth from maxsize (assuming 64 bit as the default)
|
||||
_sz = 32 if sys.maxsize == (2**31-1) else 64
|
||||
_bd = bd if bd != 0 else 9988
|
||||
# vrtl[version, release, technology_level]
|
||||
return "aix-{:1x}{:1d}{:02d}-{:04d}-{}".format(vrtl[0], vrtl[1], vrtl[2], _bd, _sz)
|
||||
|
||||
|
||||
# extract version, release and technology level from a VRMF string
|
||||
def _aix_vrtl(vrmf):
|
||||
# type: (str) -> List[int]
|
||||
v, r, tl = vrmf.split(".")[:3]
|
||||
return [int(v[-1]), int(r), int(tl)]
|
||||
|
||||
|
||||
def _aix_bos_rte():
|
||||
# type: () -> Tuple[str, int]
|
||||
"""
|
||||
Return a Tuple[str, int] e.g., ['7.1.4.34', 1806]
|
||||
The fileset bos.rte represents the current AIX run-time level. It's VRMF and
|
||||
builddate reflect the current ABI levels of the runtime environment.
|
||||
If no builddate is found give a value that will satisfy pep425 related queries
|
||||
"""
|
||||
# All AIX systems to have lslpp installed in this location
|
||||
# subprocess may not be available during python bootstrap
|
||||
try:
|
||||
import subprocess
|
||||
out = subprocess.check_output(["/usr/bin/lslpp", "-Lqc", "bos.rte"])
|
||||
except ImportError:
|
||||
out = _read_cmd_output("/usr/bin/lslpp -Lqc bos.rte")
|
||||
out = out.decode("utf-8")
|
||||
out = out.strip().split(":") # type: ignore
|
||||
_bd = int(out[-1]) if out[-1] != '' else 9988
|
||||
return (str(out[2]), _bd)
|
||||
|
||||
|
||||
def aix_platform():
|
||||
# type: () -> str
|
||||
"""
|
||||
AIX filesets are identified by four decimal values: V.R.M.F.
|
||||
V (version) and R (release) can be retrieved using ``uname``
|
||||
Since 2007, starting with AIX 5.3 TL7, the M value has been
|
||||
included with the fileset bos.rte and represents the Technology
|
||||
Level (TL) of AIX. The F (Fix) value also increases, but is not
|
||||
relevant for comparing releases and binary compatibility.
|
||||
For binary compatibility the so-called builddate is needed.
|
||||
Again, the builddate of an AIX release is associated with bos.rte.
|
||||
AIX ABI compatibility is described as guaranteed at: https://www.ibm.com/\
|
||||
support/knowledgecenter/en/ssw_aix_72/install/binary_compatability.html
|
||||
|
||||
For pep425 purposes the AIX platform tag becomes:
|
||||
"aix-{:1x}{:1d}{:02d}-{:04d}-{}".format(v, r, tl, builddate, bitsize)
|
||||
e.g., "aix-6107-1415-32" for AIX 6.1 TL7 bd 1415, 32-bit
|
||||
and, "aix-6107-1415-64" for AIX 6.1 TL7 bd 1415, 64-bit
|
||||
"""
|
||||
vrmf, bd = _aix_bos_rte()
|
||||
return _aix_tag(_aix_vrtl(vrmf), bd)
|
||||
|
||||
|
||||
# extract vrtl from the BUILD_GNU_TYPE as an int
|
||||
def _aix_bgt():
|
||||
# type: () -> List[int]
|
||||
gnu_type = sysconfig.get_config_var("BUILD_GNU_TYPE")
|
||||
if not gnu_type:
|
||||
raise ValueError("BUILD_GNU_TYPE is not defined")
|
||||
return _aix_vrtl(vrmf=gnu_type)
|
||||
|
||||
|
||||
def aix_buildtag():
|
||||
# type: () -> str
|
||||
"""
|
||||
Return the platform_tag of the system Python was built on.
|
||||
"""
|
||||
# AIX_BUILDDATE is defined by configure with:
|
||||
# lslpp -Lcq bos.rte | awk -F: '{ print $NF }'
|
||||
build_date = sysconfig.get_config_var("AIX_BUILDDATE")
|
||||
try:
|
||||
build_date = int(build_date)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(f"AIX_BUILDDATE is not defined or invalid: "
|
||||
f"{build_date!r}")
|
||||
return _aix_tag(_aix_bgt(), build_date)
|
||||
181
Lib/_android_support.py
vendored
Normal file
181
Lib/_android_support.py
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
import io
|
||||
import sys
|
||||
from threading import RLock
|
||||
from time import sleep, time
|
||||
|
||||
# The maximum length of a log message in bytes, including the level marker and
|
||||
# tag, is defined as LOGGER_ENTRY_MAX_PAYLOAD at
|
||||
# https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log.h;l=71.
|
||||
# Messages longer than this will be truncated by logcat. This limit has already
|
||||
# been reduced at least once in the history of Android (from 4076 to 4068 between
|
||||
# API level 23 and 26), so leave some headroom.
|
||||
MAX_BYTES_PER_WRITE = 4000
|
||||
|
||||
# UTF-8 uses a maximum of 4 bytes per character, so limiting text writes to this
|
||||
# size ensures that we can always avoid exceeding MAX_BYTES_PER_WRITE.
|
||||
# However, if the actual number of bytes per character is smaller than that,
|
||||
# then we may still join multiple consecutive text writes into binary
|
||||
# writes containing a larger number of characters.
|
||||
MAX_CHARS_PER_WRITE = MAX_BYTES_PER_WRITE // 4
|
||||
|
||||
|
||||
# When embedded in an app on current versions of Android, there's no easy way to
|
||||
# monitor the C-level stdout and stderr. The testbed comes with a .c file to
|
||||
# redirect them to the system log using a pipe, but that wouldn't be convenient
|
||||
# or appropriate for all apps. So we redirect at the Python level instead.
|
||||
def init_streams(android_log_write, stdout_prio, stderr_prio):
|
||||
if sys.executable:
|
||||
return # Not embedded in an app.
|
||||
|
||||
global logcat
|
||||
logcat = Logcat(android_log_write)
|
||||
|
||||
sys.stdout = TextLogStream(
|
||||
stdout_prio, "python.stdout", sys.stdout.fileno())
|
||||
sys.stderr = TextLogStream(
|
||||
stderr_prio, "python.stderr", sys.stderr.fileno())
|
||||
|
||||
|
||||
class TextLogStream(io.TextIOWrapper):
|
||||
def __init__(self, prio, tag, fileno=None, **kwargs):
|
||||
# The default is surrogateescape for stdout and backslashreplace for
|
||||
# stderr, but in the context of an Android log, readability is more
|
||||
# important than reversibility.
|
||||
kwargs.setdefault("encoding", "UTF-8")
|
||||
kwargs.setdefault("errors", "backslashreplace")
|
||||
|
||||
super().__init__(BinaryLogStream(prio, tag, fileno), **kwargs)
|
||||
self._lock = RLock()
|
||||
self._pending_bytes = []
|
||||
self._pending_bytes_count = 0
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TextLogStream {self.buffer.tag!r}>"
|
||||
|
||||
def write(self, s):
|
||||
if not isinstance(s, str):
|
||||
raise TypeError(
|
||||
f"write() argument must be str, not {type(s).__name__}")
|
||||
|
||||
# In case `s` is a str subclass that writes itself to stdout or stderr
|
||||
# when we call its methods, convert it to an actual str.
|
||||
s = str.__str__(s)
|
||||
|
||||
# We want to emit one log message per line wherever possible, so split
|
||||
# the string into lines first. Note that "".splitlines() == [], so
|
||||
# nothing will be logged for an empty string.
|
||||
with self._lock:
|
||||
for line in s.splitlines(keepends=True):
|
||||
while line:
|
||||
chunk = line[:MAX_CHARS_PER_WRITE]
|
||||
line = line[MAX_CHARS_PER_WRITE:]
|
||||
self._write_chunk(chunk)
|
||||
|
||||
return len(s)
|
||||
|
||||
# The size and behavior of TextIOWrapper's buffer is not part of its public
|
||||
# API, so we handle buffering ourselves to avoid truncation.
|
||||
def _write_chunk(self, s):
|
||||
b = s.encode(self.encoding, self.errors)
|
||||
if self._pending_bytes_count + len(b) > MAX_BYTES_PER_WRITE:
|
||||
self.flush()
|
||||
|
||||
self._pending_bytes.append(b)
|
||||
self._pending_bytes_count += len(b)
|
||||
if (
|
||||
self.write_through
|
||||
or b.endswith(b"\n")
|
||||
or self._pending_bytes_count > MAX_BYTES_PER_WRITE
|
||||
):
|
||||
self.flush()
|
||||
|
||||
def flush(self):
|
||||
with self._lock:
|
||||
self.buffer.write(b"".join(self._pending_bytes))
|
||||
self._pending_bytes.clear()
|
||||
self._pending_bytes_count = 0
|
||||
|
||||
# Since this is a line-based logging system, line buffering cannot be turned
|
||||
# off, i.e. a newline always causes a flush.
|
||||
@property
|
||||
def line_buffering(self):
|
||||
return True
|
||||
|
||||
|
||||
class BinaryLogStream(io.RawIOBase):
|
||||
def __init__(self, prio, tag, fileno=None):
|
||||
self.prio = prio
|
||||
self.tag = tag
|
||||
self._fileno = fileno
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BinaryLogStream {self.tag!r}>"
|
||||
|
||||
def writable(self):
|
||||
return True
|
||||
|
||||
def write(self, b):
|
||||
if type(b) is not bytes:
|
||||
try:
|
||||
b = bytes(memoryview(b))
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
f"write() argument must be bytes-like, not {type(b).__name__}"
|
||||
) from None
|
||||
|
||||
# Writing an empty string to the stream should have no effect.
|
||||
if b:
|
||||
logcat.write(self.prio, self.tag, b)
|
||||
return len(b)
|
||||
|
||||
# This is needed by the test suite --timeout option, which uses faulthandler.
|
||||
def fileno(self):
|
||||
if self._fileno is None:
|
||||
raise io.UnsupportedOperation("fileno")
|
||||
return self._fileno
|
||||
|
||||
|
||||
# When a large volume of data is written to logcat at once, e.g. when a test
|
||||
# module fails in --verbose3 mode, there's a risk of overflowing logcat's own
|
||||
# buffer and losing messages. We avoid this by imposing a rate limit using the
|
||||
# token bucket algorithm, based on a conservative estimate of how fast `adb
|
||||
# logcat` can consume data.
|
||||
MAX_BYTES_PER_SECOND = 1024 * 1024
|
||||
|
||||
# The logcat buffer size of a device can be determined by running `logcat -g`.
|
||||
# We set the token bucket size to half of the buffer size of our current minimum
|
||||
# API level, because other things on the system will be producing messages as
|
||||
# well.
|
||||
BUCKET_SIZE = 128 * 1024
|
||||
|
||||
# https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log_read.h;l=39
|
||||
PER_MESSAGE_OVERHEAD = 28
|
||||
|
||||
|
||||
class Logcat:
|
||||
def __init__(self, android_log_write):
|
||||
self.android_log_write = android_log_write
|
||||
self._lock = RLock()
|
||||
self._bucket_level = 0
|
||||
self._prev_write_time = time()
|
||||
|
||||
def write(self, prio, tag, message):
|
||||
# Encode null bytes using "modified UTF-8" to avoid them truncating the
|
||||
# message.
|
||||
message = message.replace(b"\x00", b"\xc0\x80")
|
||||
|
||||
with self._lock:
|
||||
now = time()
|
||||
self._bucket_level += (
|
||||
(now - self._prev_write_time) * MAX_BYTES_PER_SECOND)
|
||||
|
||||
# If the bucket level is still below zero, the clock must have gone
|
||||
# backwards, so reset it to zero and continue.
|
||||
self._bucket_level = max(0, min(self._bucket_level, BUCKET_SIZE))
|
||||
self._prev_write_time = now
|
||||
|
||||
self._bucket_level -= PER_MESSAGE_OVERHEAD + len(tag) + len(message)
|
||||
if self._bucket_level < 0:
|
||||
sleep(-self._bucket_level / MAX_BYTES_PER_SECOND)
|
||||
|
||||
self.android_log_write(prio, tag, message)
|
||||
66
Lib/_apple_support.py
vendored
Normal file
66
Lib/_apple_support.py
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
import io
|
||||
import sys
|
||||
|
||||
|
||||
def init_streams(log_write, stdout_level, stderr_level):
|
||||
# Redirect stdout and stderr to the Apple system log. This method is
|
||||
# invoked by init_apple_streams() (initconfig.c) if config->use_system_logger
|
||||
# is enabled.
|
||||
sys.stdout = SystemLog(log_write, stdout_level, errors=sys.stderr.errors)
|
||||
sys.stderr = SystemLog(log_write, stderr_level, errors=sys.stderr.errors)
|
||||
|
||||
|
||||
class SystemLog(io.TextIOWrapper):
|
||||
def __init__(self, log_write, level, **kwargs):
|
||||
kwargs.setdefault("encoding", "UTF-8")
|
||||
kwargs.setdefault("line_buffering", True)
|
||||
super().__init__(LogStream(log_write, level), **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SystemLog (level {self.buffer.level})>"
|
||||
|
||||
def write(self, s):
|
||||
if not isinstance(s, str):
|
||||
raise TypeError(
|
||||
f"write() argument must be str, not {type(s).__name__}")
|
||||
|
||||
# In case `s` is a str subclass that writes itself to stdout or stderr
|
||||
# when we call its methods, convert it to an actual str.
|
||||
s = str.__str__(s)
|
||||
|
||||
# We want to emit one log message per line, so split
|
||||
# the string before sending it to the superclass.
|
||||
for line in s.splitlines(keepends=True):
|
||||
super().write(line)
|
||||
|
||||
return len(s)
|
||||
|
||||
|
||||
class LogStream(io.RawIOBase):
|
||||
def __init__(self, log_write, level):
|
||||
self.log_write = log_write
|
||||
self.level = level
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LogStream (level {self.level!r})>"
|
||||
|
||||
def writable(self):
|
||||
return True
|
||||
|
||||
def write(self, b):
|
||||
if type(b) is not bytes:
|
||||
try:
|
||||
b = bytes(memoryview(b))
|
||||
except TypeError:
|
||||
raise TypeError(
|
||||
f"write() argument must be bytes-like, not {type(b).__name__}"
|
||||
) from None
|
||||
|
||||
# Writing an empty string to the stream should have no effect.
|
||||
if b:
|
||||
# Encode null bytes using "modified UTF-8" to avoid truncating the
|
||||
# message. This should not affect the return value, as the caller
|
||||
# may be expecting it to match the length of the input.
|
||||
self.log_write(self.level, b.replace(b"\x00", b"\xc0\x80"))
|
||||
|
||||
return len(b)
|
||||
132
Lib/_collections_abc.py
vendored
132
Lib/_collections_abc.py
vendored
@@ -6,6 +6,32 @@
|
||||
Unit tests are in test_collections.
|
||||
"""
|
||||
|
||||
############ Maintenance notes #########################################
|
||||
#
|
||||
# ABCs are different from other standard library modules in that they
|
||||
# specify compliance tests. In general, once an ABC has been published,
|
||||
# new methods (either abstract or concrete) cannot be added.
|
||||
#
|
||||
# Though classes that inherit from an ABC would automatically receive a
|
||||
# new mixin method, registered classes would become non-compliant and
|
||||
# violate the contract promised by ``isinstance(someobj, SomeABC)``.
|
||||
#
|
||||
# Though irritating, the correct procedure for adding new abstract or
|
||||
# mixin methods is to create a new ABC as a subclass of the previous
|
||||
# ABC. For example, union(), intersection(), and difference() cannot
|
||||
# be added to Set but could go into a new ABC that extends Set.
|
||||
#
|
||||
# Because they are so hard to change, new ABCs should have their APIs
|
||||
# carefully thought through prior to publication.
|
||||
#
|
||||
# Since ABCMeta only checks for the presence of methods, it is possible
|
||||
# to alter the signature of a method by adding optional arguments
|
||||
# or changing parameters names. This is still a bit dubious but at
|
||||
# least it won't cause isinstance() to return an incorrect result.
|
||||
#
|
||||
#
|
||||
#######################################################################
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import sys
|
||||
|
||||
@@ -23,7 +49,7 @@ __all__ = ["Awaitable", "Coroutine",
|
||||
"Mapping", "MutableMapping",
|
||||
"MappingView", "KeysView", "ItemsView", "ValuesView",
|
||||
"Sequence", "MutableSequence",
|
||||
"ByteString",
|
||||
"ByteString", "Buffer",
|
||||
]
|
||||
|
||||
# This module has been renamed from collections.abc to _collections_abc to
|
||||
@@ -413,6 +439,21 @@ class Collection(Sized, Iterable, Container):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class Buffer(metaclass=ABCMeta):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def __buffer__(self, flags: int, /) -> memoryview:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is Buffer:
|
||||
return _check_methods(C, "__buffer__")
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class _CallableGenericAlias(GenericAlias):
|
||||
""" Represent `Callable[argtypes, resulttype]`.
|
||||
|
||||
@@ -430,25 +471,13 @@ class _CallableGenericAlias(GenericAlias):
|
||||
raise TypeError(
|
||||
"Callable must be used as Callable[[arg, ...], result].")
|
||||
t_args, t_result = args
|
||||
if isinstance(t_args, list):
|
||||
if isinstance(t_args, (tuple, list)):
|
||||
args = (*t_args, t_result)
|
||||
elif not _is_param_expr(t_args):
|
||||
raise TypeError(f"Expected a list of types, an ellipsis, "
|
||||
f"ParamSpec, or Concatenate. Got {t_args}")
|
||||
return super().__new__(cls, origin, args)
|
||||
|
||||
@property
|
||||
def __parameters__(self):
|
||||
params = []
|
||||
for arg in self.__args__:
|
||||
# Looks like a genericalias
|
||||
if hasattr(arg, "__parameters__") and isinstance(arg.__parameters__, tuple):
|
||||
params.extend(arg.__parameters__)
|
||||
else:
|
||||
if _is_typevarlike(arg):
|
||||
params.append(arg)
|
||||
return tuple(dict.fromkeys(params))
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.__args__) == 2 and _is_param_expr(self.__args__[0]):
|
||||
return super().__repr__()
|
||||
@@ -467,55 +496,18 @@ class _CallableGenericAlias(GenericAlias):
|
||||
# rather than the default types.GenericAlias object. Most of the
|
||||
# code is copied from typing's _GenericAlias and the builtin
|
||||
# types.GenericAlias.
|
||||
|
||||
# A special case in PEP 612 where if X = Callable[P, int],
|
||||
# then X[int, str] == X[[int, str]].
|
||||
param_len = len(self.__parameters__)
|
||||
if param_len == 0:
|
||||
raise TypeError(f'{self} is not a generic class')
|
||||
if not isinstance(item, tuple):
|
||||
item = (item,)
|
||||
if (param_len == 1 and _is_param_expr(self.__parameters__[0])
|
||||
and item and not _is_param_expr(item[0])):
|
||||
item = (list(item),)
|
||||
item_len = len(item)
|
||||
if item_len != param_len:
|
||||
raise TypeError(f'Too {"many" if item_len > param_len else "few"}'
|
||||
f' arguments for {self};'
|
||||
f' actual {item_len}, expected {param_len}')
|
||||
subst = dict(zip(self.__parameters__, item))
|
||||
new_args = []
|
||||
for arg in self.__args__:
|
||||
if _is_typevarlike(arg):
|
||||
if _is_param_expr(arg):
|
||||
arg = subst[arg]
|
||||
if not _is_param_expr(arg):
|
||||
raise TypeError(f"Expected a list of types, an ellipsis, "
|
||||
f"ParamSpec, or Concatenate. Got {arg}")
|
||||
else:
|
||||
arg = subst[arg]
|
||||
# Looks like a GenericAlias
|
||||
elif hasattr(arg, '__parameters__') and isinstance(arg.__parameters__, tuple):
|
||||
subparams = arg.__parameters__
|
||||
if subparams:
|
||||
subargs = tuple(subst[x] for x in subparams)
|
||||
arg = arg[subargs]
|
||||
new_args.append(arg)
|
||||
|
||||
new_args = super().__getitem__(item).__args__
|
||||
|
||||
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
|
||||
if not isinstance(new_args[0], list):
|
||||
if not isinstance(new_args[0], (tuple, list)):
|
||||
t_result = new_args[-1]
|
||||
t_args = new_args[:-1]
|
||||
new_args = (t_args, t_result)
|
||||
return _CallableGenericAlias(Callable, tuple(new_args))
|
||||
|
||||
|
||||
def _is_typevarlike(arg):
|
||||
obj = type(arg)
|
||||
# looks like a TypeVar/ParamSpec
|
||||
return (obj.__module__ == 'typing'
|
||||
and obj.__name__ in {'ParamSpec', 'TypeVar'})
|
||||
|
||||
def _is_param_expr(obj):
|
||||
"""Checks if obj matches either a list of types, ``...``, ``ParamSpec`` or
|
||||
``_ConcatenateGenericAlias`` from typing.py
|
||||
@@ -533,9 +525,8 @@ def _type_repr(obj):
|
||||
|
||||
Copied from :mod:`typing` since collections.abc
|
||||
shouldn't depend on that module.
|
||||
(Keep this roughly in sync with the typing version.)
|
||||
"""
|
||||
if isinstance(obj, GenericAlias):
|
||||
return repr(obj)
|
||||
if isinstance(obj, type):
|
||||
if obj.__module__ == 'builtins':
|
||||
return obj.__qualname__
|
||||
@@ -868,7 +859,7 @@ class KeysView(MappingView, Set):
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def _from_iterable(self, it):
|
||||
def _from_iterable(cls, it):
|
||||
return set(it)
|
||||
|
||||
def __contains__(self, key):
|
||||
@@ -886,7 +877,7 @@ class ItemsView(MappingView, Set):
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def _from_iterable(self, it):
|
||||
def _from_iterable(cls, it):
|
||||
return set(it)
|
||||
|
||||
def __contains__(self, item):
|
||||
@@ -1064,10 +1055,10 @@ class Sequence(Reversible, Collection):
|
||||
while stop is None or i < stop:
|
||||
try:
|
||||
v = self[i]
|
||||
if v is value or v == value:
|
||||
return i
|
||||
except IndexError:
|
||||
break
|
||||
if v is value or v == value:
|
||||
return i
|
||||
i += 1
|
||||
raise ValueError
|
||||
|
||||
@@ -1080,8 +1071,27 @@ Sequence.register(str)
|
||||
Sequence.register(range)
|
||||
Sequence.register(memoryview)
|
||||
|
||||
class _DeprecateByteStringMeta(ABCMeta):
|
||||
def __new__(cls, name, bases, namespace, **kwargs):
|
||||
if name != "ByteString":
|
||||
import warnings
|
||||
|
||||
class ByteString(Sequence):
|
||||
warnings._deprecated(
|
||||
"collections.abc.ByteString",
|
||||
remove=(3, 14),
|
||||
)
|
||||
return super().__new__(cls, name, bases, namespace, **kwargs)
|
||||
|
||||
def __instancecheck__(cls, instance):
|
||||
import warnings
|
||||
|
||||
warnings._deprecated(
|
||||
"collections.abc.ByteString",
|
||||
remove=(3, 14),
|
||||
)
|
||||
return super().__instancecheck__(instance)
|
||||
|
||||
class ByteString(Sequence, metaclass=_DeprecateByteStringMeta):
|
||||
"""This unifies bytes and bytearray.
|
||||
|
||||
XXX Should add all their methods.
|
||||
|
||||
67
Lib/_colorize.py
vendored
Normal file
67
Lib/_colorize.py
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
COLORIZE = True
|
||||
|
||||
|
||||
class ANSIColors:
|
||||
BOLD_GREEN = "\x1b[1;32m"
|
||||
BOLD_MAGENTA = "\x1b[1;35m"
|
||||
BOLD_RED = "\x1b[1;31m"
|
||||
GREEN = "\x1b[32m"
|
||||
GREY = "\x1b[90m"
|
||||
MAGENTA = "\x1b[35m"
|
||||
RED = "\x1b[31m"
|
||||
RESET = "\x1b[0m"
|
||||
YELLOW = "\x1b[33m"
|
||||
|
||||
|
||||
NoColors = ANSIColors()
|
||||
|
||||
for attr in dir(NoColors):
|
||||
if not attr.startswith("__"):
|
||||
setattr(NoColors, attr, "")
|
||||
|
||||
|
||||
def get_colors(colorize: bool = False, *, file=None) -> ANSIColors:
|
||||
if colorize or can_colorize(file=file):
|
||||
return ANSIColors()
|
||||
else:
|
||||
return NoColors
|
||||
|
||||
|
||||
def can_colorize(*, file=None) -> bool:
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
|
||||
if not sys.flags.ignore_environment:
|
||||
if os.environ.get("PYTHON_COLORS") == "0":
|
||||
return False
|
||||
if os.environ.get("PYTHON_COLORS") == "1":
|
||||
return True
|
||||
if os.environ.get("NO_COLOR"):
|
||||
return False
|
||||
if not COLORIZE:
|
||||
return False
|
||||
if os.environ.get("FORCE_COLOR"):
|
||||
return True
|
||||
if os.environ.get("TERM") == "dumb":
|
||||
return False
|
||||
|
||||
if not hasattr(file, "fileno"):
|
||||
return False
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import nt
|
||||
|
||||
if not nt._supports_virtual_terminal():
|
||||
return False
|
||||
except (ImportError, AttributeError):
|
||||
return False
|
||||
|
||||
try:
|
||||
return os.isatty(file.fileno())
|
||||
except io.UnsupportedOperation:
|
||||
return file.isatty()
|
||||
12
Lib/_compression.py
vendored
12
Lib/_compression.py
vendored
@@ -1,7 +1,7 @@
|
||||
"""Internal classes used by the gzip, lzma and bz2 modules"""
|
||||
|
||||
import io
|
||||
|
||||
import sys
|
||||
|
||||
BUFFER_SIZE = io.DEFAULT_BUFFER_SIZE # Compressed data read chunk size
|
||||
|
||||
@@ -110,6 +110,16 @@ class DecompressReader(io.RawIOBase):
|
||||
self._pos += len(data)
|
||||
return data
|
||||
|
||||
def readall(self):
|
||||
chunks = []
|
||||
# sys.maxsize means the max length of output buffer is unlimited,
|
||||
# so that the whole input buffer can be decompressed within one
|
||||
# .decompress() call.
|
||||
while data := self.read(sys.maxsize):
|
||||
chunks.append(data)
|
||||
|
||||
return b"".join(chunks)
|
||||
|
||||
# Rewind the file to the beginning of the data stream.
|
||||
def _rewind(self):
|
||||
self._fp.seek(0)
|
||||
|
||||
22
Lib/_dummy_os.py
vendored
22
Lib/_dummy_os.py
vendored
@@ -5,22 +5,30 @@ A shim of the os module containing only simple path-related utilities
|
||||
try:
|
||||
from os import *
|
||||
except ImportError:
|
||||
import abc
|
||||
import abc, sys
|
||||
|
||||
def __getattr__(name):
|
||||
raise OSError("no os specific module found")
|
||||
if name in {"_path_normpath", "__path__"}:
|
||||
raise AttributeError(name)
|
||||
if name.isupper():
|
||||
return 0
|
||||
def dummy(*args, **kwargs):
|
||||
import io
|
||||
return io.UnsupportedOperation(f"{name}: no os specific module found")
|
||||
dummy.__name__ = f"dummy_{name}"
|
||||
return dummy
|
||||
|
||||
def _shim():
|
||||
import _dummy_os, sys
|
||||
sys.modules['os'] = _dummy_os
|
||||
sys.modules['os.path'] = _dummy_os.path
|
||||
sys.modules['os'] = sys.modules['posix'] = sys.modules[__name__]
|
||||
|
||||
import posixpath as path
|
||||
import sys
|
||||
sys.modules['os.path'] = path
|
||||
del sys
|
||||
|
||||
sep = path.sep
|
||||
supports_dir_fd = set()
|
||||
supports_effective_ids = set()
|
||||
supports_fd = set()
|
||||
supports_follow_symlinks = set()
|
||||
|
||||
|
||||
def fspath(path):
|
||||
|
||||
3
Lib/_dummy_thread.py
vendored
3
Lib/_dummy_thread.py
vendored
@@ -145,6 +145,9 @@ class LockType(object):
|
||||
def locked(self):
|
||||
return self.locked_status
|
||||
|
||||
def _at_fork_reinit(self):
|
||||
self.locked_status = False
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s.%s object at %s>" % (
|
||||
"locked" if self.locked_status else "unlocked",
|
||||
|
||||
71
Lib/_ios_support.py
vendored
Normal file
71
Lib/_ios_support.py
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import sys
|
||||
try:
|
||||
from ctypes import cdll, c_void_p, c_char_p, util
|
||||
except ImportError:
|
||||
# ctypes is an optional module. If it's not present, we're limited in what
|
||||
# we can tell about the system, but we don't want to prevent the module
|
||||
# from working.
|
||||
print("ctypes isn't available; iOS system calls will not be available", file=sys.stderr)
|
||||
objc = None
|
||||
else:
|
||||
# ctypes is available. Load the ObjC library, and wrap the objc_getClass,
|
||||
# sel_registerName methods
|
||||
lib = util.find_library("objc")
|
||||
if lib is None:
|
||||
# Failed to load the objc library
|
||||
raise ImportError("ObjC runtime library couldn't be loaded")
|
||||
|
||||
objc = cdll.LoadLibrary(lib)
|
||||
objc.objc_getClass.restype = c_void_p
|
||||
objc.objc_getClass.argtypes = [c_char_p]
|
||||
objc.sel_registerName.restype = c_void_p
|
||||
objc.sel_registerName.argtypes = [c_char_p]
|
||||
|
||||
|
||||
def get_platform_ios():
|
||||
# Determine if this is a simulator using the multiarch value
|
||||
is_simulator = sys.implementation._multiarch.endswith("simulator")
|
||||
|
||||
# We can't use ctypes; abort
|
||||
if not objc:
|
||||
return None
|
||||
|
||||
# Most of the methods return ObjC objects
|
||||
objc.objc_msgSend.restype = c_void_p
|
||||
# All the methods used have no arguments.
|
||||
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
|
||||
|
||||
# Equivalent of:
|
||||
# device = [UIDevice currentDevice]
|
||||
UIDevice = objc.objc_getClass(b"UIDevice")
|
||||
SEL_currentDevice = objc.sel_registerName(b"currentDevice")
|
||||
device = objc.objc_msgSend(UIDevice, SEL_currentDevice)
|
||||
|
||||
# Equivalent of:
|
||||
# device_systemVersion = [device systemVersion]
|
||||
SEL_systemVersion = objc.sel_registerName(b"systemVersion")
|
||||
device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)
|
||||
|
||||
# Equivalent of:
|
||||
# device_systemName = [device systemName]
|
||||
SEL_systemName = objc.sel_registerName(b"systemName")
|
||||
device_systemName = objc.objc_msgSend(device, SEL_systemName)
|
||||
|
||||
# Equivalent of:
|
||||
# device_model = [device model]
|
||||
SEL_model = objc.sel_registerName(b"model")
|
||||
device_model = objc.objc_msgSend(device, SEL_model)
|
||||
|
||||
# UTF8String returns a const char*;
|
||||
SEL_UTF8String = objc.sel_registerName(b"UTF8String")
|
||||
objc.objc_msgSend.restype = c_char_p
|
||||
|
||||
# Equivalent of:
|
||||
# system = [device_systemName UTF8String]
|
||||
# release = [device_systemVersion UTF8String]
|
||||
# model = [device_model UTF8String]
|
||||
system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
|
||||
release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
|
||||
model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()
|
||||
|
||||
return system, release, model, is_simulator
|
||||
35
Lib/_markupbase.py
vendored
35
Lib/_markupbase.py
vendored
@@ -29,10 +29,6 @@ class ParserBase:
|
||||
raise RuntimeError(
|
||||
"_markupbase.ParserBase must be subclassed")
|
||||
|
||||
def error(self, message):
|
||||
raise NotImplementedError(
|
||||
"subclasses of ParserBase must override error()")
|
||||
|
||||
def reset(self):
|
||||
self.lineno = 1
|
||||
self.offset = 0
|
||||
@@ -131,12 +127,11 @@ class ParserBase:
|
||||
# also in data attribute specifications of attlist declaration
|
||||
# also link type declaration subsets in linktype declarations
|
||||
# also link attribute specification lists in link declarations
|
||||
self.error("unsupported '[' char in %s declaration" % decltype)
|
||||
raise AssertionError("unsupported '[' char in %s declaration" % decltype)
|
||||
else:
|
||||
self.error("unexpected '[' char in declaration")
|
||||
raise AssertionError("unexpected '[' char in declaration")
|
||||
else:
|
||||
self.error(
|
||||
"unexpected %r char in declaration" % rawdata[j])
|
||||
raise AssertionError("unexpected %r char in declaration" % rawdata[j])
|
||||
if j < 0:
|
||||
return j
|
||||
return -1 # incomplete
|
||||
@@ -156,7 +151,9 @@ class ParserBase:
|
||||
# look for MS Office ]> ending
|
||||
match= _msmarkedsectionclose.search(rawdata, i+3)
|
||||
else:
|
||||
self.error('unknown status keyword %r in marked section' % rawdata[i+3:j])
|
||||
raise AssertionError(
|
||||
'unknown status keyword %r in marked section' % rawdata[i+3:j]
|
||||
)
|
||||
if not match:
|
||||
return -1
|
||||
if report:
|
||||
@@ -168,7 +165,7 @@ class ParserBase:
|
||||
def parse_comment(self, i, report=1):
|
||||
rawdata = self.rawdata
|
||||
if rawdata[i:i+4] != '<!--':
|
||||
self.error('unexpected call to parse_comment()')
|
||||
raise AssertionError('unexpected call to parse_comment()')
|
||||
match = _commentclose.search(rawdata, i+4)
|
||||
if not match:
|
||||
return -1
|
||||
@@ -192,7 +189,9 @@ class ParserBase:
|
||||
return -1
|
||||
if s != "<!":
|
||||
self.updatepos(declstartpos, j + 1)
|
||||
self.error("unexpected char in internal subset (in %r)" % s)
|
||||
raise AssertionError(
|
||||
"unexpected char in internal subset (in %r)" % s
|
||||
)
|
||||
if (j + 2) == n:
|
||||
# end of buffer; incomplete
|
||||
return -1
|
||||
@@ -209,8 +208,9 @@ class ParserBase:
|
||||
return -1
|
||||
if name not in {"attlist", "element", "entity", "notation"}:
|
||||
self.updatepos(declstartpos, j + 2)
|
||||
self.error(
|
||||
"unknown declaration %r in internal subset" % name)
|
||||
raise AssertionError(
|
||||
"unknown declaration %r in internal subset" % name
|
||||
)
|
||||
# handle the individual names
|
||||
meth = getattr(self, "_parse_doctype_" + name)
|
||||
j = meth(j, declstartpos)
|
||||
@@ -234,14 +234,14 @@ class ParserBase:
|
||||
if rawdata[j] == ">":
|
||||
return j
|
||||
self.updatepos(declstartpos, j)
|
||||
self.error("unexpected char after internal subset")
|
||||
raise AssertionError("unexpected char after internal subset")
|
||||
else:
|
||||
return -1
|
||||
elif c.isspace():
|
||||
j = j + 1
|
||||
else:
|
||||
self.updatepos(declstartpos, j)
|
||||
self.error("unexpected char %r in internal subset" % c)
|
||||
raise AssertionError("unexpected char %r in internal subset" % c)
|
||||
# end of buffer reached
|
||||
return -1
|
||||
|
||||
@@ -387,8 +387,9 @@ class ParserBase:
|
||||
return name.lower(), m.end()
|
||||
else:
|
||||
self.updatepos(declstartpos, i)
|
||||
self.error("expected name token at %r"
|
||||
% rawdata[declstartpos:declstartpos+20])
|
||||
raise AssertionError(
|
||||
"expected name token at %r" % rawdata[declstartpos:declstartpos+20]
|
||||
)
|
||||
|
||||
# To be overridden -- handlers for unknown objects
|
||||
def unknown_decl(self, data):
|
||||
|
||||
5
Lib/_osx_support.py
vendored
5
Lib/_osx_support.py
vendored
@@ -507,6 +507,11 @@ def get_platform_osx(_config_vars, osname, release, machine):
|
||||
# MACOSX_DEPLOYMENT_TARGET.
|
||||
|
||||
macver = _config_vars.get('MACOSX_DEPLOYMENT_TARGET', '')
|
||||
if macver and '.' not in macver:
|
||||
# Ensure that the version includes at least a major
|
||||
# and minor version, even if MACOSX_DEPLOYMENT_TARGET
|
||||
# is set to a single-label version like "14".
|
||||
macver += '.0'
|
||||
macrelease = _get_system_version() or macver
|
||||
macver = macver or macrelease
|
||||
|
||||
|
||||
6
Lib/_py_abc.py
vendored
6
Lib/_py_abc.py
vendored
@@ -33,6 +33,8 @@ class ABCMeta(type):
|
||||
_abc_invalidation_counter = 0
|
||||
|
||||
def __new__(mcls, name, bases, namespace, /, **kwargs):
|
||||
# TODO: RUSTPYTHON remove this line (prevents duplicate bases)
|
||||
bases = tuple(dict.fromkeys(bases))
|
||||
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
||||
# Compute set of abstract method names
|
||||
abstracts = {name
|
||||
@@ -98,8 +100,8 @@ class ABCMeta(type):
|
||||
subtype = type(instance)
|
||||
if subtype is subclass:
|
||||
if (cls._abc_negative_cache_version ==
|
||||
ABCMeta._abc_invalidation_counter and
|
||||
subclass in cls._abc_negative_cache):
|
||||
ABCMeta._abc_invalidation_counter and
|
||||
subclass in cls._abc_negative_cache):
|
||||
return False
|
||||
# Fall back to the subclass check.
|
||||
return cls.__subclasscheck__(subclass)
|
||||
|
||||
11
Lib/_pycodecs.py
vendored
11
Lib/_pycodecs.py
vendored
@@ -1086,11 +1086,13 @@ def charmapencode_output(c, mapping):
|
||||
rep = mapping[c]
|
||||
if isinstance(rep, int) or isinstance(rep, int):
|
||||
if rep < 256:
|
||||
return rep
|
||||
return [rep]
|
||||
else:
|
||||
raise TypeError("character mapping must be in range(256)")
|
||||
elif isinstance(rep, str):
|
||||
return ord(rep)
|
||||
return [ord(rep)]
|
||||
elif isinstance(rep, bytes):
|
||||
return rep
|
||||
elif rep == None:
|
||||
raise KeyError("character maps to <undefined>")
|
||||
else:
|
||||
@@ -1113,12 +1115,13 @@ def PyUnicode_EncodeCharmap(p, size, mapping='latin-1', errors='strict'):
|
||||
#/* try to encode it */
|
||||
try:
|
||||
x = charmapencode_output(ord(p[inpos]), mapping)
|
||||
res += [x]
|
||||
res += x
|
||||
except KeyError:
|
||||
x = unicode_call_errorhandler(errors, "charmap",
|
||||
"character maps to <undefined>", p, inpos, inpos+1, False)
|
||||
try:
|
||||
res += [charmapencode_output(ord(y), mapping) for y in x[0]]
|
||||
for y in x[0]:
|
||||
res += charmapencode_output(ord(y), mapping)
|
||||
except KeyError:
|
||||
raise UnicodeEncodeError("charmap", p, inpos, inpos+1,
|
||||
"character maps to <undefined>")
|
||||
|
||||
2643
Lib/_pydatetime.py
vendored
Normal file
2643
Lib/_pydatetime.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
27
Lib/_pydecimal.py
vendored
27
Lib/_pydecimal.py
vendored
@@ -734,18 +734,23 @@ class Decimal(object):
|
||||
|
||||
"""
|
||||
if isinstance(f, int): # handle integer inputs
|
||||
return cls(f)
|
||||
if not isinstance(f, float):
|
||||
raise TypeError("argument must be int or float.")
|
||||
if _math.isinf(f) or _math.isnan(f):
|
||||
return cls(repr(f))
|
||||
if _math.copysign(1.0, f) == 1.0:
|
||||
sign = 0
|
||||
sign = 0 if f >= 0 else 1
|
||||
k = 0
|
||||
coeff = str(abs(f))
|
||||
elif isinstance(f, float):
|
||||
if _math.isinf(f) or _math.isnan(f):
|
||||
return cls(repr(f))
|
||||
if _math.copysign(1.0, f) == 1.0:
|
||||
sign = 0
|
||||
else:
|
||||
sign = 1
|
||||
n, d = abs(f).as_integer_ratio()
|
||||
k = d.bit_length() - 1
|
||||
coeff = str(n*5**k)
|
||||
else:
|
||||
sign = 1
|
||||
n, d = abs(f).as_integer_ratio()
|
||||
k = d.bit_length() - 1
|
||||
result = _dec_from_triple(sign, str(n*5**k), -k)
|
||||
raise TypeError("argument must be int or float.")
|
||||
|
||||
result = _dec_from_triple(sign, coeff, -k)
|
||||
if cls is Decimal:
|
||||
return result
|
||||
else:
|
||||
|
||||
90
Lib/_pyio.py
vendored
90
Lib/_pyio.py
vendored
@@ -44,8 +44,9 @@ def text_encoding(encoding, stacklevel=2):
|
||||
"""
|
||||
A helper function to choose the text encoding.
|
||||
|
||||
When encoding is not None, just return it.
|
||||
Otherwise, return the default text encoding (i.e. "locale").
|
||||
When encoding is not None, this function returns it.
|
||||
Otherwise, this function returns the default text encoding
|
||||
(i.e. "locale" or "utf-8" depends on UTF-8 mode).
|
||||
|
||||
This function emits an EncodingWarning if *encoding* is None and
|
||||
sys.flags.warn_default_encoding is true.
|
||||
@@ -55,7 +56,10 @@ def text_encoding(encoding, stacklevel=2):
|
||||
However, please consider using encoding="utf-8" for new APIs.
|
||||
"""
|
||||
if encoding is None:
|
||||
encoding = "locale"
|
||||
if sys.flags.utf8_mode:
|
||||
encoding = "utf-8"
|
||||
else:
|
||||
encoding = "locale"
|
||||
if sys.flags.warn_default_encoding:
|
||||
import warnings
|
||||
warnings.warn("'encoding' argument not specified.",
|
||||
@@ -101,7 +105,6 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
|
||||
'b' binary mode
|
||||
't' text mode (default)
|
||||
'+' open a disk file for updating (reading and writing)
|
||||
'U' universal newline mode (deprecated)
|
||||
========= ===============================================================
|
||||
|
||||
The default mode is 'rt' (open for reading text). For binary random
|
||||
@@ -117,10 +120,6 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
|
||||
returned as strings, the bytes having been first decoded using a
|
||||
platform-dependent encoding or using the specified encoding if given.
|
||||
|
||||
'U' mode is deprecated and will raise an exception in future versions
|
||||
of Python. It has no effect in Python 3. Use newline to control
|
||||
universal newlines mode.
|
||||
|
||||
buffering is an optional integer used to set the buffering policy.
|
||||
Pass 0 to switch buffering off (only allowed in binary mode), 1 to select
|
||||
line buffering (only usable in text mode), and an integer > 1 to indicate
|
||||
@@ -206,7 +205,7 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
|
||||
if errors is not None and not isinstance(errors, str):
|
||||
raise TypeError("invalid errors: %r" % errors)
|
||||
modes = set(mode)
|
||||
if modes - set("axrwb+tU") or len(mode) > len(modes):
|
||||
if modes - set("axrwb+t") or len(mode) > len(modes):
|
||||
raise ValueError("invalid mode: %r" % mode)
|
||||
creating = "x" in modes
|
||||
reading = "r" in modes
|
||||
@@ -215,13 +214,6 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
|
||||
updating = "+" in modes
|
||||
text = "t" in modes
|
||||
binary = "b" in modes
|
||||
if "U" in modes:
|
||||
if creating or writing or appending or updating:
|
||||
raise ValueError("mode U cannot be combined with 'x', 'w', 'a', or '+'")
|
||||
import warnings
|
||||
warnings.warn("'U' mode is deprecated",
|
||||
DeprecationWarning, 2)
|
||||
reading = True
|
||||
if text and binary:
|
||||
raise ValueError("can't have text and binary mode at once")
|
||||
if creating + reading + writing + appending > 1:
|
||||
@@ -311,22 +303,6 @@ except AttributeError:
|
||||
open_code = _open_code_with_warning
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "OpenWrapper":
|
||||
# bpo-43680: Until Python 3.9, _pyio.open was not a static method and
|
||||
# builtins.open was set to OpenWrapper to not become a bound method
|
||||
# when set to a class variable. _io.open is a built-in function whereas
|
||||
# _pyio.open is a Python function. In Python 3.10, _pyio.open() is now
|
||||
# a static method, and builtins.open() is now io.open().
|
||||
import warnings
|
||||
warnings.warn('OpenWrapper is deprecated, use open instead',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
global OpenWrapper
|
||||
OpenWrapper = open
|
||||
return OpenWrapper
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
# In normal operation, both `UnsupportedOperation`s should be bound to the
|
||||
# same object.
|
||||
try:
|
||||
@@ -338,8 +314,7 @@ except AttributeError:
|
||||
|
||||
class IOBase(metaclass=abc.ABCMeta):
|
||||
|
||||
"""The abstract base class for all I/O classes, acting on streams of
|
||||
bytes. There is no public constructor.
|
||||
"""The abstract base class for all I/O classes.
|
||||
|
||||
This class provides dummy implementations for many methods that
|
||||
derived classes can override selectively; the default implementations
|
||||
@@ -1154,6 +1129,7 @@ class BufferedReader(_BufferedIOMixin):
|
||||
do at most one raw read to satisfy it. We never return more
|
||||
than self.buffer_size.
|
||||
"""
|
||||
self._checkClosed("peek of closed file")
|
||||
with self._read_lock:
|
||||
return self._peek_unlocked(size)
|
||||
|
||||
@@ -1172,6 +1148,7 @@ class BufferedReader(_BufferedIOMixin):
|
||||
"""Reads up to size bytes, with at most one read() system call."""
|
||||
# Returns up to size bytes. If at least one byte is buffered, we
|
||||
# only return buffered bytes. Otherwise, we do one raw read.
|
||||
self._checkClosed("read of closed file")
|
||||
if size < 0:
|
||||
size = self.buffer_size
|
||||
if size == 0:
|
||||
@@ -1189,6 +1166,8 @@ class BufferedReader(_BufferedIOMixin):
|
||||
def _readinto(self, buf, read1):
|
||||
"""Read data into *buf* with at most one system call."""
|
||||
|
||||
self._checkClosed("readinto of closed file")
|
||||
|
||||
# Need to create a memoryview object of type 'b', otherwise
|
||||
# we may not be able to assign bytes to it, and slicing it
|
||||
# would create a new object.
|
||||
@@ -1233,11 +1212,13 @@ class BufferedReader(_BufferedIOMixin):
|
||||
return written
|
||||
|
||||
def tell(self):
|
||||
return _BufferedIOMixin.tell(self) - len(self._read_buf) + self._read_pos
|
||||
# GH-95782: Keep return value non-negative
|
||||
return max(_BufferedIOMixin.tell(self) - len(self._read_buf) + self._read_pos, 0)
|
||||
|
||||
def seek(self, pos, whence=0):
|
||||
if whence not in valid_seek_flags:
|
||||
raise ValueError("invalid whence value")
|
||||
self._checkClosed("seek of closed file")
|
||||
with self._read_lock:
|
||||
if whence == 1:
|
||||
pos -= len(self._read_buf) - self._read_pos
|
||||
@@ -1845,7 +1826,7 @@ class TextIOBase(IOBase):
|
||||
"""Base class for text I/O.
|
||||
|
||||
This class provides a character and line based interface to stream
|
||||
I/O. There is no public constructor.
|
||||
I/O.
|
||||
"""
|
||||
|
||||
def read(self, size=-1):
|
||||
@@ -1997,7 +1978,7 @@ class TextIOWrapper(TextIOBase):
|
||||
r"""Character and line based layer over a BufferedIOBase object, buffer.
|
||||
|
||||
encoding gives the name of the encoding that the stream will be
|
||||
decoded or encoded with. It defaults to locale.getpreferredencoding(False).
|
||||
decoded or encoded with. It defaults to locale.getencoding().
|
||||
|
||||
errors determines the strictness of encoding and decoding (see the
|
||||
codecs.register) and defaults to "strict".
|
||||
@@ -2031,19 +2012,7 @@ class TextIOWrapper(TextIOBase):
|
||||
encoding = text_encoding(encoding)
|
||||
|
||||
if encoding == "locale":
|
||||
try:
|
||||
encoding = os.device_encoding(buffer.fileno()) or "locale"
|
||||
except (AttributeError, UnsupportedOperation):
|
||||
pass
|
||||
|
||||
if encoding == "locale":
|
||||
try:
|
||||
import locale
|
||||
except ImportError:
|
||||
# Importing locale may fail if Python is being built
|
||||
encoding = "utf-8"
|
||||
else:
|
||||
encoding = locale.getpreferredencoding(False)
|
||||
encoding = self._get_locale_encoding()
|
||||
|
||||
if not isinstance(encoding, str):
|
||||
raise ValueError("invalid encoding: %r" % encoding)
|
||||
@@ -2176,6 +2145,8 @@ class TextIOWrapper(TextIOBase):
|
||||
else:
|
||||
if not isinstance(encoding, str):
|
||||
raise TypeError("invalid encoding: %r" % encoding)
|
||||
if encoding == "locale":
|
||||
encoding = self._get_locale_encoding()
|
||||
|
||||
if newline is Ellipsis:
|
||||
newline = self._readnl
|
||||
@@ -2243,8 +2214,9 @@ class TextIOWrapper(TextIOBase):
|
||||
self.buffer.write(b)
|
||||
if self._line_buffering and (haslf or "\r" in s):
|
||||
self.flush()
|
||||
self._set_decoded_chars('')
|
||||
self._snapshot = None
|
||||
if self._snapshot is not None:
|
||||
self._set_decoded_chars('')
|
||||
self._snapshot = None
|
||||
if self._decoder:
|
||||
self._decoder.reset()
|
||||
return length
|
||||
@@ -2280,6 +2252,15 @@ class TextIOWrapper(TextIOBase):
|
||||
self._decoded_chars_used += len(chars)
|
||||
return chars
|
||||
|
||||
def _get_locale_encoding(self):
|
||||
try:
|
||||
import locale
|
||||
except ImportError:
|
||||
# Importing locale may fail if Python is being built
|
||||
return "utf-8"
|
||||
else:
|
||||
return locale.getencoding()
|
||||
|
||||
def _rewind_decoded_chars(self, n):
|
||||
"""Rewind the _decoded_chars buffer."""
|
||||
if self._decoded_chars_used < n:
|
||||
@@ -2549,8 +2530,9 @@ class TextIOWrapper(TextIOBase):
|
||||
# Read everything.
|
||||
result = (self._get_decoded_chars() +
|
||||
decoder.decode(self.buffer.read(), final=True))
|
||||
self._set_decoded_chars('')
|
||||
self._snapshot = None
|
||||
if self._snapshot is not None:
|
||||
self._set_decoded_chars('')
|
||||
self._snapshot = None
|
||||
return result
|
||||
else:
|
||||
# Keep reading chunks until we have size characters to return.
|
||||
|
||||
363
Lib/_pylong.py
vendored
Normal file
363
Lib/_pylong.py
vendored
Normal file
@@ -0,0 +1,363 @@
|
||||
"""Python implementations of some algorithms for use by longobject.c.
|
||||
The goal is to provide asymptotically faster algorithms that can be
|
||||
used for operations on integers with many digits. In those cases, the
|
||||
performance overhead of the Python implementation is not significant
|
||||
since the asymptotic behavior is what dominates runtime. Functions
|
||||
provided by this module should be considered private and not part of any
|
||||
public API.
|
||||
|
||||
Note: for ease of maintainability, please prefer clear code and avoid
|
||||
"micro-optimizations". This module will only be imported and used for
|
||||
integers with a huge number of digits. Saving a few microseconds with
|
||||
tricky or non-obvious code is not worth it. For people looking for
|
||||
maximum performance, they should use something like gmpy2."""
|
||||
|
||||
import re
|
||||
import decimal
|
||||
try:
|
||||
import _decimal
|
||||
except ImportError:
|
||||
_decimal = None
|
||||
|
||||
# A number of functions have this form, where `w` is a desired number of
|
||||
# digits in base `base`:
|
||||
#
|
||||
# def inner(...w...):
|
||||
# if w <= LIMIT:
|
||||
# return something
|
||||
# lo = w >> 1
|
||||
# hi = w - lo
|
||||
# something involving base**lo, inner(...lo...), j, and inner(...hi...)
|
||||
# figure out largest w needed
|
||||
# result = inner(w)
|
||||
#
|
||||
# They all had some on-the-fly scheme to cache `base**lo` results for reuse.
|
||||
# Power is costly.
|
||||
#
|
||||
# This routine aims to compute all amd only the needed powers in advance, as
|
||||
# efficiently as reasonably possible. This isn't trivial, and all the
|
||||
# on-the-fly methods did needless work in many cases. The driving code above
|
||||
# changes to:
|
||||
#
|
||||
# figure out largest w needed
|
||||
# mycache = compute_powers(w, base, LIMIT)
|
||||
# result = inner(w)
|
||||
#
|
||||
# and `mycache[lo]` replaces `base**lo` in the inner function.
|
||||
#
|
||||
# While this does give minor speedups (a few percent at best), the primary
|
||||
# intent is to simplify the functions using this, by eliminating the need for
|
||||
# them to craft their own ad-hoc caching schemes.
|
||||
def compute_powers(w, base, more_than, show=False):
|
||||
seen = set()
|
||||
need = set()
|
||||
ws = {w}
|
||||
while ws:
|
||||
w = ws.pop() # any element is fine to use next
|
||||
if w in seen or w <= more_than:
|
||||
continue
|
||||
seen.add(w)
|
||||
lo = w >> 1
|
||||
# only _need_ lo here; some other path may, or may not, need hi
|
||||
need.add(lo)
|
||||
ws.add(lo)
|
||||
if w & 1:
|
||||
ws.add(lo + 1)
|
||||
|
||||
d = {}
|
||||
if not need:
|
||||
return d
|
||||
it = iter(sorted(need))
|
||||
first = next(it)
|
||||
if show:
|
||||
print("pow at", first)
|
||||
d[first] = base ** first
|
||||
for this in it:
|
||||
if this - 1 in d:
|
||||
if show:
|
||||
print("* base at", this)
|
||||
d[this] = d[this - 1] * base # cheap
|
||||
else:
|
||||
lo = this >> 1
|
||||
hi = this - lo
|
||||
assert lo in d
|
||||
if show:
|
||||
print("square at", this)
|
||||
# Multiplying a bigint by itself (same object!) is about twice
|
||||
# as fast in CPython.
|
||||
sq = d[lo] * d[lo]
|
||||
if hi != lo:
|
||||
assert hi == lo + 1
|
||||
if show:
|
||||
print(" and * base")
|
||||
sq *= base
|
||||
d[this] = sq
|
||||
return d
|
||||
|
||||
_unbounded_dec_context = decimal.getcontext().copy()
|
||||
_unbounded_dec_context.prec = decimal.MAX_PREC
|
||||
_unbounded_dec_context.Emax = decimal.MAX_EMAX
|
||||
_unbounded_dec_context.Emin = decimal.MIN_EMIN
|
||||
_unbounded_dec_context.traps[decimal.Inexact] = 1 # sanity check
|
||||
|
||||
def int_to_decimal(n):
|
||||
"""Asymptotically fast conversion of an 'int' to Decimal."""
|
||||
|
||||
# Function due to Tim Peters. See GH issue #90716 for details.
|
||||
# https://github.com/python/cpython/issues/90716
|
||||
#
|
||||
# The implementation in longobject.c of base conversion algorithms
|
||||
# between power-of-2 and non-power-of-2 bases are quadratic time.
|
||||
# This function implements a divide-and-conquer algorithm that is
|
||||
# faster for large numbers. Builds an equal decimal.Decimal in a
|
||||
# "clever" recursive way. If we want a string representation, we
|
||||
# apply str to _that_.
|
||||
|
||||
from decimal import Decimal as D
|
||||
BITLIM = 200
|
||||
|
||||
# Don't bother caching the "lo" mask in this; the time to compute it is
|
||||
# tiny compared to the multiply.
|
||||
def inner(n, w):
|
||||
if w <= BITLIM:
|
||||
return D(n)
|
||||
w2 = w >> 1
|
||||
hi = n >> w2
|
||||
lo = n & ((1 << w2) - 1)
|
||||
return inner(lo, w2) + inner(hi, w - w2) * w2pow[w2]
|
||||
|
||||
with decimal.localcontext(_unbounded_dec_context):
|
||||
nbits = n.bit_length()
|
||||
w2pow = compute_powers(nbits, D(2), BITLIM)
|
||||
if n < 0:
|
||||
negate = True
|
||||
n = -n
|
||||
else:
|
||||
negate = False
|
||||
result = inner(n, nbits)
|
||||
if negate:
|
||||
result = -result
|
||||
return result
|
||||
|
||||
def int_to_decimal_string(n):
|
||||
"""Asymptotically fast conversion of an 'int' to a decimal string."""
|
||||
w = n.bit_length()
|
||||
if w > 450_000 and _decimal is not None:
|
||||
# It is only usable with the C decimal implementation.
|
||||
# _pydecimal.py calls str() on very large integers, which in its
|
||||
# turn calls int_to_decimal_string(), causing very deep recursion.
|
||||
return str(int_to_decimal(n))
|
||||
|
||||
# Fallback algorithm for the case when the C decimal module isn't
|
||||
# available. This algorithm is asymptotically worse than the algorithm
|
||||
# using the decimal module, but better than the quadratic time
|
||||
# implementation in longobject.c.
|
||||
|
||||
DIGLIM = 1000
|
||||
def inner(n, w):
|
||||
if w <= DIGLIM:
|
||||
return str(n)
|
||||
w2 = w >> 1
|
||||
hi, lo = divmod(n, pow10[w2])
|
||||
return inner(hi, w - w2) + inner(lo, w2).zfill(w2)
|
||||
|
||||
# The estimation of the number of decimal digits.
|
||||
# There is no harm in small error. If we guess too large, there may
|
||||
# be leading 0's that need to be stripped. If we guess too small, we
|
||||
# may need to call str() recursively for the remaining highest digits,
|
||||
# which can still potentially be a large integer. This is manifested
|
||||
# only if the number has way more than 10**15 digits, that exceeds
|
||||
# the 52-bit physical address limit in both Intel64 and AMD64.
|
||||
w = int(w * 0.3010299956639812 + 1) # log10(2)
|
||||
pow10 = compute_powers(w, 5, DIGLIM)
|
||||
for k, v in pow10.items():
|
||||
pow10[k] = v << k # 5**k << k == 5**k * 2**k == 10**k
|
||||
if n < 0:
|
||||
n = -n
|
||||
sign = '-'
|
||||
else:
|
||||
sign = ''
|
||||
s = inner(n, w)
|
||||
if s[0] == '0' and n:
|
||||
# If our guess of w is too large, there may be leading 0's that
|
||||
# need to be stripped.
|
||||
s = s.lstrip('0')
|
||||
return sign + s
|
||||
|
||||
def _str_to_int_inner(s):
|
||||
"""Asymptotically fast conversion of a 'str' to an 'int'."""
|
||||
|
||||
# Function due to Bjorn Martinsson. See GH issue #90716 for details.
|
||||
# https://github.com/python/cpython/issues/90716
|
||||
#
|
||||
# The implementation in longobject.c of base conversion algorithms
|
||||
# between power-of-2 and non-power-of-2 bases are quadratic time.
|
||||
# This function implements a divide-and-conquer algorithm making use
|
||||
# of Python's built in big int multiplication. Since Python uses the
|
||||
# Karatsuba algorithm for multiplication, the time complexity
|
||||
# of this function is O(len(s)**1.58).
|
||||
|
||||
DIGLIM = 2048
|
||||
|
||||
def inner(a, b):
|
||||
if b - a <= DIGLIM:
|
||||
return int(s[a:b])
|
||||
mid = (a + b + 1) >> 1
|
||||
return (inner(mid, b)
|
||||
+ ((inner(a, mid) * w5pow[b - mid])
|
||||
<< (b - mid)))
|
||||
|
||||
w5pow = compute_powers(len(s), 5, DIGLIM)
|
||||
return inner(0, len(s))
|
||||
|
||||
|
||||
def int_from_string(s):
|
||||
"""Asymptotically fast version of PyLong_FromString(), conversion
|
||||
of a string of decimal digits into an 'int'."""
|
||||
# PyLong_FromString() has already removed leading +/-, checked for invalid
|
||||
# use of underscore characters, checked that string consists of only digits
|
||||
# and underscores, and stripped leading whitespace. The input can still
|
||||
# contain underscores and have trailing whitespace.
|
||||
s = s.rstrip().replace('_', '')
|
||||
return _str_to_int_inner(s)
|
||||
|
||||
def str_to_int(s):
|
||||
"""Asymptotically fast version of decimal string to 'int' conversion."""
|
||||
# FIXME: this doesn't support the full syntax that int() supports.
|
||||
m = re.match(r'\s*([+-]?)([0-9_]+)\s*', s)
|
||||
if not m:
|
||||
raise ValueError('invalid literal for int() with base 10')
|
||||
v = int_from_string(m.group(2))
|
||||
if m.group(1) == '-':
|
||||
v = -v
|
||||
return v
|
||||
|
||||
|
||||
# Fast integer division, based on code from Mark Dickinson, fast_div.py
|
||||
# GH-47701. Additional refinements and optimizations by Bjorn Martinsson. The
|
||||
# algorithm is due to Burnikel and Ziegler, in their paper "Fast Recursive
|
||||
# Division".
|
||||
|
||||
_DIV_LIMIT = 4000
|
||||
|
||||
|
||||
def _div2n1n(a, b, n):
|
||||
"""Divide a 2n-bit nonnegative integer a by an n-bit positive integer
|
||||
b, using a recursive divide-and-conquer algorithm.
|
||||
|
||||
Inputs:
|
||||
n is a positive integer
|
||||
b is a positive integer with exactly n bits
|
||||
a is a nonnegative integer such that a < 2**n * b
|
||||
|
||||
Output:
|
||||
(q, r) such that a = b*q+r and 0 <= r < b.
|
||||
|
||||
"""
|
||||
if a.bit_length() - n <= _DIV_LIMIT:
|
||||
return divmod(a, b)
|
||||
pad = n & 1
|
||||
if pad:
|
||||
a <<= 1
|
||||
b <<= 1
|
||||
n += 1
|
||||
half_n = n >> 1
|
||||
mask = (1 << half_n) - 1
|
||||
b1, b2 = b >> half_n, b & mask
|
||||
q1, r = _div3n2n(a >> n, (a >> half_n) & mask, b, b1, b2, half_n)
|
||||
q2, r = _div3n2n(r, a & mask, b, b1, b2, half_n)
|
||||
if pad:
|
||||
r >>= 1
|
||||
return q1 << half_n | q2, r
|
||||
|
||||
|
||||
def _div3n2n(a12, a3, b, b1, b2, n):
|
||||
"""Helper function for _div2n1n; not intended to be called directly."""
|
||||
if a12 >> n == b1:
|
||||
q, r = (1 << n) - 1, a12 - (b1 << n) + b1
|
||||
else:
|
||||
q, r = _div2n1n(a12, b1, n)
|
||||
r = (r << n | a3) - q * b2
|
||||
while r < 0:
|
||||
q -= 1
|
||||
r += b
|
||||
return q, r
|
||||
|
||||
|
||||
def _int2digits(a, n):
|
||||
"""Decompose non-negative int a into base 2**n
|
||||
|
||||
Input:
|
||||
a is a non-negative integer
|
||||
|
||||
Output:
|
||||
List of the digits of a in base 2**n in little-endian order,
|
||||
meaning the most significant digit is last. The most
|
||||
significant digit is guaranteed to be non-zero.
|
||||
If a is 0 then the output is an empty list.
|
||||
|
||||
"""
|
||||
a_digits = [0] * ((a.bit_length() + n - 1) // n)
|
||||
|
||||
def inner(x, L, R):
|
||||
if L + 1 == R:
|
||||
a_digits[L] = x
|
||||
return
|
||||
mid = (L + R) >> 1
|
||||
shift = (mid - L) * n
|
||||
upper = x >> shift
|
||||
lower = x ^ (upper << shift)
|
||||
inner(lower, L, mid)
|
||||
inner(upper, mid, R)
|
||||
|
||||
if a:
|
||||
inner(a, 0, len(a_digits))
|
||||
return a_digits
|
||||
|
||||
|
||||
def _digits2int(digits, n):
|
||||
"""Combine base-2**n digits into an int. This function is the
|
||||
inverse of `_int2digits`. For more details, see _int2digits.
|
||||
"""
|
||||
|
||||
def inner(L, R):
|
||||
if L + 1 == R:
|
||||
return digits[L]
|
||||
mid = (L + R) >> 1
|
||||
shift = (mid - L) * n
|
||||
return (inner(mid, R) << shift) + inner(L, mid)
|
||||
|
||||
return inner(0, len(digits)) if digits else 0
|
||||
|
||||
|
||||
def _divmod_pos(a, b):
|
||||
"""Divide a non-negative integer a by a positive integer b, giving
|
||||
quotient and remainder."""
|
||||
# Use grade-school algorithm in base 2**n, n = nbits(b)
|
||||
n = b.bit_length()
|
||||
a_digits = _int2digits(a, n)
|
||||
|
||||
r = 0
|
||||
q_digits = []
|
||||
for a_digit in reversed(a_digits):
|
||||
q_digit, r = _div2n1n((r << n) + a_digit, b, n)
|
||||
q_digits.append(q_digit)
|
||||
q_digits.reverse()
|
||||
q = _digits2int(q_digits, n)
|
||||
return q, r
|
||||
|
||||
|
||||
def int_divmod(a, b):
|
||||
"""Asymptotically fast replacement for divmod, for 'int'.
|
||||
Its time complexity is O(n**1.58), where n = #bits(a) + #bits(b).
|
||||
"""
|
||||
if b == 0:
|
||||
raise ZeroDivisionError
|
||||
elif b < 0:
|
||||
q, r = int_divmod(-a, -b)
|
||||
return q, -r
|
||||
elif a < 0:
|
||||
q, r = int_divmod(~a, b)
|
||||
return ~q, b + ~r
|
||||
else:
|
||||
return _divmod_pos(a, b)
|
||||
19
Lib/_pyrepl/__init__.py
vendored
Normal file
19
Lib/_pyrepl/__init__.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Copyright 2000-2008 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
6
Lib/_pyrepl/__main__.py
vendored
Normal file
6
Lib/_pyrepl/__main__.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Important: don't add things to this module, as they will end up in the REPL's
|
||||
# default globals. Use _pyrepl.main instead.
|
||||
|
||||
if __name__ == "__main__":
|
||||
from .main import interactive_console as __pyrepl_interactive_console
|
||||
__pyrepl_interactive_console()
|
||||
68
Lib/_pyrepl/_minimal_curses.py
vendored
Normal file
68
Lib/_pyrepl/_minimal_curses.py
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Minimal '_curses' module, the low-level interface for curses module
|
||||
which is not meant to be used directly.
|
||||
|
||||
Based on ctypes. It's too incomplete to be really called '_curses', so
|
||||
to use it, you have to import it and stick it in sys.modules['_curses']
|
||||
manually.
|
||||
|
||||
Note that there is also a built-in module _minimal_curses which will
|
||||
hide this one if compiled in.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
|
||||
|
||||
class error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _find_clib() -> str:
|
||||
trylibs = ["ncursesw", "ncurses", "curses"]
|
||||
|
||||
for lib in trylibs:
|
||||
path = ctypes.util.find_library(lib)
|
||||
if path:
|
||||
return path
|
||||
raise ModuleNotFoundError("curses library not found", name="_pyrepl._minimal_curses")
|
||||
|
||||
|
||||
_clibpath = _find_clib()
|
||||
clib = ctypes.cdll.LoadLibrary(_clibpath)
|
||||
|
||||
clib.setupterm.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)]
|
||||
clib.setupterm.restype = ctypes.c_int
|
||||
|
||||
clib.tigetstr.argtypes = [ctypes.c_char_p]
|
||||
clib.tigetstr.restype = ctypes.c_ssize_t
|
||||
|
||||
clib.tparm.argtypes = [ctypes.c_char_p] + 9 * [ctypes.c_int] # type: ignore[operator]
|
||||
clib.tparm.restype = ctypes.c_char_p
|
||||
|
||||
OK = 0
|
||||
ERR = -1
|
||||
|
||||
# ____________________________________________________________
|
||||
|
||||
|
||||
def setupterm(termstr, fd):
|
||||
err = ctypes.c_int(0)
|
||||
result = clib.setupterm(termstr, fd, ctypes.byref(err))
|
||||
if result == ERR:
|
||||
raise error("setupterm() failed (err=%d)" % err.value)
|
||||
|
||||
|
||||
def tigetstr(cap):
|
||||
if not isinstance(cap, bytes):
|
||||
cap = cap.encode("ascii")
|
||||
result = clib.tigetstr(cap)
|
||||
if result == ERR:
|
||||
return None
|
||||
return ctypes.cast(result, ctypes.c_char_p).value
|
||||
|
||||
|
||||
def tparm(str, i1=0, i2=0, i3=0, i4=0, i5=0, i6=0, i7=0, i8=0, i9=0):
|
||||
result = clib.tparm(str, i1, i2, i3, i4, i5, i6, i7, i8, i9)
|
||||
if result is None:
|
||||
raise error("tparm() returned NULL")
|
||||
return result
|
||||
74
Lib/_pyrepl/_threading_handler.py
vendored
Normal file
74
Lib/_pyrepl/_threading_handler.py
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import traceback
|
||||
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from threading import Thread
|
||||
from types import TracebackType
|
||||
from typing import Protocol
|
||||
|
||||
class ExceptHookArgs(Protocol):
|
||||
@property
|
||||
def exc_type(self) -> type[BaseException]: ...
|
||||
@property
|
||||
def exc_value(self) -> BaseException | None: ...
|
||||
@property
|
||||
def exc_traceback(self) -> TracebackType | None: ...
|
||||
@property
|
||||
def thread(self) -> Thread | None: ...
|
||||
|
||||
class ShowExceptions(Protocol):
|
||||
def __call__(self) -> int: ...
|
||||
def add(self, s: str) -> None: ...
|
||||
|
||||
from .reader import Reader
|
||||
|
||||
|
||||
def install_threading_hook(reader: Reader) -> None:
|
||||
import threading
|
||||
|
||||
@dataclass
|
||||
class ExceptHookHandler:
|
||||
lock: threading.Lock = field(default_factory=threading.Lock)
|
||||
messages: list[str] = field(default_factory=list)
|
||||
|
||||
def show(self) -> int:
|
||||
count = 0
|
||||
with self.lock:
|
||||
if not self.messages:
|
||||
return 0
|
||||
reader.restore()
|
||||
for tb in self.messages:
|
||||
count += 1
|
||||
if tb:
|
||||
print(tb)
|
||||
self.messages.clear()
|
||||
reader.scheduled_commands.append("ctrl-c")
|
||||
reader.prepare()
|
||||
return count
|
||||
|
||||
def add(self, s: str) -> None:
|
||||
with self.lock:
|
||||
self.messages.append(s)
|
||||
|
||||
def exception(self, args: ExceptHookArgs) -> None:
|
||||
lines = traceback.format_exception(
|
||||
args.exc_type,
|
||||
args.exc_value,
|
||||
args.exc_traceback,
|
||||
colorize=reader.can_colorize,
|
||||
) # type: ignore[call-overload]
|
||||
pre = f"\nException in {args.thread.name}:\n" if args.thread else "\n"
|
||||
tb = pre + "".join(lines)
|
||||
self.add(tb)
|
||||
|
||||
def __call__(self) -> int:
|
||||
return self.show()
|
||||
|
||||
|
||||
handler = ExceptHookHandler()
|
||||
reader.threading_hook = handler
|
||||
threading.excepthook = handler.exception
|
||||
489
Lib/_pyrepl/commands.py
vendored
Normal file
489
Lib/_pyrepl/commands.py
vendored
Normal file
@@ -0,0 +1,489 @@
|
||||
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Antonio Cuni
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
|
||||
# Categories of actions:
|
||||
# killing
|
||||
# yanking
|
||||
# motion
|
||||
# editing
|
||||
# history
|
||||
# finishing
|
||||
# [completion]
|
||||
|
||||
|
||||
# types
|
||||
if False:
|
||||
from .historical_reader import HistoricalReader
|
||||
|
||||
|
||||
class Command:
|
||||
finish: bool = False
|
||||
kills_digit_arg: bool = True
|
||||
|
||||
def __init__(
|
||||
self, reader: HistoricalReader, event_name: str, event: list[str]
|
||||
) -> None:
|
||||
# Reader should really be "any reader" but there's too much usage of
|
||||
# HistoricalReader methods and fields in the code below for us to
|
||||
# refactor at the moment.
|
||||
|
||||
self.reader = reader
|
||||
self.event = event
|
||||
self.event_name = event_name
|
||||
|
||||
def do(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class KillCommand(Command):
|
||||
def kill_range(self, start: int, end: int) -> None:
|
||||
if start == end:
|
||||
return
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
text = b[start:end]
|
||||
del b[start:end]
|
||||
if is_kill(r.last_command):
|
||||
if start < r.pos:
|
||||
r.kill_ring[-1] = text + r.kill_ring[-1]
|
||||
else:
|
||||
r.kill_ring[-1] = r.kill_ring[-1] + text
|
||||
else:
|
||||
r.kill_ring.append(text)
|
||||
r.pos = start
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class YankCommand(Command):
|
||||
pass
|
||||
|
||||
|
||||
class MotionCommand(Command):
|
||||
pass
|
||||
|
||||
|
||||
class EditCommand(Command):
|
||||
pass
|
||||
|
||||
|
||||
class FinishCommand(Command):
|
||||
finish = True
|
||||
pass
|
||||
|
||||
|
||||
def is_kill(command: type[Command] | None) -> bool:
|
||||
return command is not None and issubclass(command, KillCommand)
|
||||
|
||||
|
||||
def is_yank(command: type[Command] | None) -> bool:
|
||||
return command is not None and issubclass(command, YankCommand)
|
||||
|
||||
|
||||
# etc
|
||||
|
||||
|
||||
class digit_arg(Command):
|
||||
kills_digit_arg = False
|
||||
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
c = self.event[-1]
|
||||
if c == "-":
|
||||
if r.arg is not None:
|
||||
r.arg = -r.arg
|
||||
else:
|
||||
r.arg = -1
|
||||
else:
|
||||
d = int(c)
|
||||
if r.arg is None:
|
||||
r.arg = d
|
||||
else:
|
||||
if r.arg < 0:
|
||||
r.arg = 10 * r.arg - d
|
||||
else:
|
||||
r.arg = 10 * r.arg + d
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class clear_screen(Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.console.clear()
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class refresh(Command):
|
||||
def do(self) -> None:
|
||||
self.reader.dirty = True
|
||||
|
||||
|
||||
class repaint(Command):
|
||||
def do(self) -> None:
|
||||
self.reader.dirty = True
|
||||
self.reader.console.repaint()
|
||||
|
||||
|
||||
class kill_line(KillCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
eol = r.eol()
|
||||
for c in b[r.pos : eol]:
|
||||
if not c.isspace():
|
||||
self.kill_range(r.pos, eol)
|
||||
return
|
||||
else:
|
||||
self.kill_range(r.pos, eol + 1)
|
||||
|
||||
|
||||
class unix_line_discard(KillCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
self.kill_range(r.bol(), r.pos)
|
||||
|
||||
|
||||
class unix_word_rubout(KillCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
for i in range(r.get_arg()):
|
||||
self.kill_range(r.bow(), r.pos)
|
||||
|
||||
|
||||
class kill_word(KillCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
for i in range(r.get_arg()):
|
||||
self.kill_range(r.pos, r.eow())
|
||||
|
||||
|
||||
class backward_kill_word(KillCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
for i in range(r.get_arg()):
|
||||
self.kill_range(r.bow(), r.pos)
|
||||
|
||||
|
||||
class yank(YankCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
if not r.kill_ring:
|
||||
r.error("nothing to yank")
|
||||
return
|
||||
r.insert(r.kill_ring[-1])
|
||||
|
||||
|
||||
class yank_pop(YankCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
if not r.kill_ring:
|
||||
r.error("nothing to yank")
|
||||
return
|
||||
if not is_yank(r.last_command):
|
||||
r.error("previous command was not a yank")
|
||||
return
|
||||
repl = len(r.kill_ring[-1])
|
||||
r.kill_ring.insert(0, r.kill_ring.pop())
|
||||
t = r.kill_ring[-1]
|
||||
b[r.pos - repl : r.pos] = t
|
||||
r.pos = r.pos - repl + len(t)
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class interrupt(FinishCommand):
|
||||
def do(self) -> None:
|
||||
import signal
|
||||
|
||||
self.reader.console.finish()
|
||||
self.reader.finish()
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
|
||||
class ctrl_c(Command):
|
||||
def do(self) -> None:
|
||||
self.reader.console.finish()
|
||||
self.reader.finish()
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
class suspend(Command):
|
||||
def do(self) -> None:
|
||||
import signal
|
||||
|
||||
r = self.reader
|
||||
p = r.pos
|
||||
r.console.finish()
|
||||
os.kill(os.getpid(), signal.SIGSTOP)
|
||||
## this should probably be done
|
||||
## in a handler for SIGCONT?
|
||||
r.console.prepare()
|
||||
r.pos = p
|
||||
# r.posxy = 0, 0 # XXX this is invalid
|
||||
r.dirty = True
|
||||
r.console.screen = []
|
||||
|
||||
|
||||
class up(MotionCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
for _ in range(r.get_arg()):
|
||||
x, y = r.pos2xy()
|
||||
new_y = y - 1
|
||||
|
||||
if r.bol() == 0:
|
||||
if r.historyi > 0:
|
||||
r.select_item(r.historyi - 1)
|
||||
return
|
||||
r.pos = 0
|
||||
r.error("start of buffer")
|
||||
return
|
||||
|
||||
if (
|
||||
x
|
||||
> (
|
||||
new_x := r.max_column(new_y)
|
||||
) # we're past the end of the previous line
|
||||
or x == r.max_column(y)
|
||||
and any(
|
||||
not i.isspace() for i in r.buffer[r.bol() :]
|
||||
) # move between eols
|
||||
):
|
||||
x = new_x
|
||||
|
||||
r.setpos_from_xy(x, new_y)
|
||||
|
||||
|
||||
class down(MotionCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
for _ in range(r.get_arg()):
|
||||
x, y = r.pos2xy()
|
||||
new_y = y + 1
|
||||
|
||||
if r.eol() == len(b):
|
||||
if r.historyi < len(r.history):
|
||||
r.select_item(r.historyi + 1)
|
||||
r.pos = r.eol(0)
|
||||
return
|
||||
r.pos = len(b)
|
||||
r.error("end of buffer")
|
||||
return
|
||||
|
||||
if (
|
||||
x
|
||||
> (
|
||||
new_x := r.max_column(new_y)
|
||||
) # we're past the end of the previous line
|
||||
or x == r.max_column(y)
|
||||
and any(
|
||||
not i.isspace() for i in r.buffer[r.bol() :]
|
||||
) # move between eols
|
||||
):
|
||||
x = new_x
|
||||
|
||||
r.setpos_from_xy(x, new_y)
|
||||
|
||||
|
||||
class left(MotionCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
for _ in range(r.get_arg()):
|
||||
p = r.pos - 1
|
||||
if p >= 0:
|
||||
r.pos = p
|
||||
else:
|
||||
self.reader.error("start of buffer")
|
||||
|
||||
|
||||
class right(MotionCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
for _ in range(r.get_arg()):
|
||||
p = r.pos + 1
|
||||
if p <= len(b):
|
||||
r.pos = p
|
||||
else:
|
||||
self.reader.error("end of buffer")
|
||||
|
||||
|
||||
class beginning_of_line(MotionCommand):
|
||||
def do(self) -> None:
|
||||
self.reader.pos = self.reader.bol()
|
||||
|
||||
|
||||
class end_of_line(MotionCommand):
|
||||
def do(self) -> None:
|
||||
self.reader.pos = self.reader.eol()
|
||||
|
||||
|
||||
class home(MotionCommand):
|
||||
def do(self) -> None:
|
||||
self.reader.pos = 0
|
||||
|
||||
|
||||
class end(MotionCommand):
|
||||
def do(self) -> None:
|
||||
self.reader.pos = len(self.reader.buffer)
|
||||
|
||||
|
||||
class forward_word(MotionCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
for i in range(r.get_arg()):
|
||||
r.pos = r.eow()
|
||||
|
||||
|
||||
class backward_word(MotionCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
for i in range(r.get_arg()):
|
||||
r.pos = r.bow()
|
||||
|
||||
|
||||
class self_insert(EditCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
text = self.event * r.get_arg()
|
||||
r.insert(text)
|
||||
|
||||
|
||||
class insert_nl(EditCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.insert("\n" * r.get_arg())
|
||||
|
||||
|
||||
class transpose_characters(EditCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
s = r.pos - 1
|
||||
if s < 0:
|
||||
r.error("cannot transpose at start of buffer")
|
||||
else:
|
||||
if s == len(b):
|
||||
s -= 1
|
||||
t = min(s + r.get_arg(), len(b) - 1)
|
||||
c = b[s]
|
||||
del b[s]
|
||||
b.insert(t, c)
|
||||
r.pos = t
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class backspace(EditCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
for i in range(r.get_arg()):
|
||||
if r.pos > 0:
|
||||
r.pos -= 1
|
||||
del b[r.pos]
|
||||
r.dirty = True
|
||||
else:
|
||||
self.reader.error("can't backspace at start")
|
||||
|
||||
|
||||
class delete(EditCommand):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
if (
|
||||
r.pos == 0
|
||||
and len(b) == 0 # this is something of a hack
|
||||
and self.event[-1] == "\004"
|
||||
):
|
||||
r.update_screen()
|
||||
r.console.finish()
|
||||
raise EOFError
|
||||
for i in range(r.get_arg()):
|
||||
if r.pos != len(b):
|
||||
del b[r.pos]
|
||||
r.dirty = True
|
||||
else:
|
||||
self.reader.error("end of buffer")
|
||||
|
||||
|
||||
class accept(FinishCommand):
|
||||
def do(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class help(Command):
|
||||
def do(self) -> None:
|
||||
import _sitebuiltins
|
||||
|
||||
with self.reader.suspend():
|
||||
self.reader.msg = _sitebuiltins._Helper()() # type: ignore[assignment, call-arg]
|
||||
|
||||
|
||||
class invalid_key(Command):
|
||||
def do(self) -> None:
|
||||
pending = self.reader.console.getpending()
|
||||
s = "".join(self.event) + pending.data
|
||||
self.reader.error("`%r' not bound" % s)
|
||||
|
||||
|
||||
class invalid_command(Command):
|
||||
def do(self) -> None:
|
||||
s = self.event_name
|
||||
self.reader.error("command `%s' not known" % s)
|
||||
|
||||
|
||||
class show_history(Command):
|
||||
def do(self) -> None:
|
||||
from .pager import get_pager
|
||||
from site import gethistoryfile # type: ignore[attr-defined]
|
||||
|
||||
history = os.linesep.join(self.reader.history[:])
|
||||
self.reader.console.restore()
|
||||
pager = get_pager()
|
||||
pager(history, gethistoryfile())
|
||||
self.reader.console.prepare()
|
||||
|
||||
# We need to copy over the state so that it's consistent between
|
||||
# console and reader, and console does not overwrite/append stuff
|
||||
self.reader.console.screen = self.reader.screen.copy()
|
||||
self.reader.console.posxy = self.reader.cxy
|
||||
|
||||
|
||||
class paste_mode(Command):
|
||||
|
||||
def do(self) -> None:
|
||||
self.reader.paste_mode = not self.reader.paste_mode
|
||||
self.reader.dirty = True
|
||||
|
||||
|
||||
class enable_bracketed_paste(Command):
|
||||
def do(self) -> None:
|
||||
self.reader.paste_mode = True
|
||||
self.reader.in_bracketed_paste = True
|
||||
|
||||
class disable_bracketed_paste(Command):
|
||||
def do(self) -> None:
|
||||
self.reader.paste_mode = False
|
||||
self.reader.in_bracketed_paste = False
|
||||
self.reader.dirty = True
|
||||
295
Lib/_pyrepl/completing_reader.py
vendored
Normal file
295
Lib/_pyrepl/completing_reader.py
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Antonio Cuni
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import re
|
||||
from . import commands, console, reader
|
||||
from .reader import Reader
|
||||
|
||||
|
||||
# types
|
||||
Command = commands.Command
|
||||
if False:
|
||||
from .types import KeySpec, CommandName
|
||||
|
||||
|
||||
def prefix(wordlist: list[str], j: int = 0) -> str:
|
||||
d = {}
|
||||
i = j
|
||||
try:
|
||||
while 1:
|
||||
for word in wordlist:
|
||||
d[word[i]] = 1
|
||||
if len(d) > 1:
|
||||
return wordlist[0][j:i]
|
||||
i += 1
|
||||
d = {}
|
||||
except IndexError:
|
||||
return wordlist[0][j:i]
|
||||
return ""
|
||||
|
||||
|
||||
STRIPCOLOR_REGEX = re.compile(r"\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[m|K]")
|
||||
|
||||
def stripcolor(s: str) -> str:
|
||||
return STRIPCOLOR_REGEX.sub('', s)
|
||||
|
||||
|
||||
def real_len(s: str) -> int:
|
||||
return len(stripcolor(s))
|
||||
|
||||
|
||||
def left_align(s: str, maxlen: int) -> str:
|
||||
stripped = stripcolor(s)
|
||||
if len(stripped) > maxlen:
|
||||
# too bad, we remove the color
|
||||
return stripped[:maxlen]
|
||||
padding = maxlen - len(stripped)
|
||||
return s + ' '*padding
|
||||
|
||||
|
||||
def build_menu(
|
||||
cons: console.Console,
|
||||
wordlist: list[str],
|
||||
start: int,
|
||||
use_brackets: bool,
|
||||
sort_in_column: bool,
|
||||
) -> tuple[list[str], int]:
|
||||
if use_brackets:
|
||||
item = "[ %s ]"
|
||||
padding = 4
|
||||
else:
|
||||
item = "%s "
|
||||
padding = 2
|
||||
maxlen = min(max(map(real_len, wordlist)), cons.width - padding)
|
||||
cols = int(cons.width / (maxlen + padding))
|
||||
rows = int((len(wordlist) - 1)/cols + 1)
|
||||
|
||||
if sort_in_column:
|
||||
# sort_in_column=False (default) sort_in_column=True
|
||||
# A B C A D G
|
||||
# D E F B E
|
||||
# G C F
|
||||
#
|
||||
# "fill" the table with empty words, so we always have the same amout
|
||||
# of rows for each column
|
||||
missing = cols*rows - len(wordlist)
|
||||
wordlist = wordlist + ['']*missing
|
||||
indexes = [(i % cols) * rows + i // cols for i in range(len(wordlist))]
|
||||
wordlist = [wordlist[i] for i in indexes]
|
||||
menu = []
|
||||
i = start
|
||||
for r in range(rows):
|
||||
row = []
|
||||
for col in range(cols):
|
||||
row.append(item % left_align(wordlist[i], maxlen))
|
||||
i += 1
|
||||
if i >= len(wordlist):
|
||||
break
|
||||
menu.append(''.join(row))
|
||||
if i >= len(wordlist):
|
||||
i = 0
|
||||
break
|
||||
if r + 5 > cons.height:
|
||||
menu.append(" %d more... " % (len(wordlist) - i))
|
||||
break
|
||||
return menu, i
|
||||
|
||||
# this gets somewhat user interface-y, and as a result the logic gets
|
||||
# very convoluted.
|
||||
#
|
||||
# To summarise the summary of the summary:- people are a problem.
|
||||
# -- The Hitch-Hikers Guide to the Galaxy, Episode 12
|
||||
|
||||
#### Desired behaviour of the completions commands.
|
||||
# the considerations are:
|
||||
# (1) how many completions are possible
|
||||
# (2) whether the last command was a completion
|
||||
# (3) if we can assume that the completer is going to return the same set of
|
||||
# completions: this is controlled by the ``assume_immutable_completions``
|
||||
# variable on the reader, which is True by default to match the historical
|
||||
# behaviour of pyrepl, but e.g. False in the ReadlineAlikeReader to match
|
||||
# more closely readline's semantics (this is needed e.g. by
|
||||
# fancycompleter)
|
||||
#
|
||||
# if there's no possible completion, beep at the user and point this out.
|
||||
# this is easy.
|
||||
#
|
||||
# if there's only one possible completion, stick it in. if the last thing
|
||||
# user did was a completion, point out that he isn't getting anywhere, but
|
||||
# only if the ``assume_immutable_completions`` is True.
|
||||
#
|
||||
# now it gets complicated.
|
||||
#
|
||||
# for the first press of a completion key:
|
||||
# if there's a common prefix, stick it in.
|
||||
|
||||
# irrespective of whether anything got stuck in, if the word is now
|
||||
# complete, show the "complete but not unique" message
|
||||
|
||||
# if there's no common prefix and if the word is not now complete,
|
||||
# beep.
|
||||
|
||||
# common prefix -> yes no
|
||||
# word complete \/
|
||||
# yes "cbnu" "cbnu"
|
||||
# no - beep
|
||||
|
||||
# for the second bang on the completion key
|
||||
# there will necessarily be no common prefix
|
||||
# show a menu of the choices.
|
||||
|
||||
# for subsequent bangs, rotate the menu around (if there are sufficient
|
||||
# choices).
|
||||
|
||||
|
||||
class complete(commands.Command):
|
||||
def do(self) -> None:
|
||||
r: CompletingReader
|
||||
r = self.reader # type: ignore[assignment]
|
||||
last_is_completer = r.last_command_is(self.__class__)
|
||||
immutable_completions = r.assume_immutable_completions
|
||||
completions_unchangable = last_is_completer and immutable_completions
|
||||
stem = r.get_stem()
|
||||
if not completions_unchangable:
|
||||
r.cmpltn_menu_choices = r.get_completions(stem)
|
||||
|
||||
completions = r.cmpltn_menu_choices
|
||||
if not completions:
|
||||
r.error("no matches")
|
||||
elif len(completions) == 1:
|
||||
if completions_unchangable and len(completions[0]) == len(stem):
|
||||
r.msg = "[ sole completion ]"
|
||||
r.dirty = True
|
||||
r.insert(completions[0][len(stem):])
|
||||
else:
|
||||
p = prefix(completions, len(stem))
|
||||
if p:
|
||||
r.insert(p)
|
||||
if last_is_completer:
|
||||
r.cmpltn_menu_visible = True
|
||||
r.cmpltn_message_visible = False
|
||||
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
|
||||
r.console, completions, r.cmpltn_menu_end,
|
||||
r.use_brackets, r.sort_in_column)
|
||||
r.dirty = True
|
||||
elif not r.cmpltn_menu_visible:
|
||||
r.cmpltn_message_visible = True
|
||||
if stem + p in completions:
|
||||
r.msg = "[ complete but not unique ]"
|
||||
r.dirty = True
|
||||
else:
|
||||
r.msg = "[ not unique ]"
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class self_insert(commands.self_insert):
|
||||
def do(self) -> None:
|
||||
r: CompletingReader
|
||||
r = self.reader # type: ignore[assignment]
|
||||
|
||||
commands.self_insert.do(self)
|
||||
if r.cmpltn_menu_visible:
|
||||
stem = r.get_stem()
|
||||
if len(stem) < 1:
|
||||
r.cmpltn_reset()
|
||||
else:
|
||||
completions = [w for w in r.cmpltn_menu_choices
|
||||
if w.startswith(stem)]
|
||||
if completions:
|
||||
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
|
||||
r.console, completions, 0,
|
||||
r.use_brackets, r.sort_in_column)
|
||||
else:
|
||||
r.cmpltn_reset()
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompletingReader(Reader):
|
||||
"""Adds completion support"""
|
||||
|
||||
### Class variables
|
||||
# see the comment for the complete command
|
||||
assume_immutable_completions = True
|
||||
use_brackets = True # display completions inside []
|
||||
sort_in_column = False
|
||||
|
||||
### Instance variables
|
||||
cmpltn_menu: list[str] = field(init=False)
|
||||
cmpltn_menu_visible: bool = field(init=False)
|
||||
cmpltn_message_visible: bool = field(init=False)
|
||||
cmpltn_menu_end: int = field(init=False)
|
||||
cmpltn_menu_choices: list[str] = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
self.cmpltn_reset()
|
||||
for c in (complete, self_insert):
|
||||
self.commands[c.__name__] = c
|
||||
self.commands[c.__name__.replace('_', '-')] = c
|
||||
|
||||
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
|
||||
return super().collect_keymap() + (
|
||||
(r'\t', 'complete'),)
|
||||
|
||||
def after_command(self, cmd: Command) -> None:
|
||||
super().after_command(cmd)
|
||||
if not isinstance(cmd, (complete, self_insert)):
|
||||
self.cmpltn_reset()
|
||||
|
||||
def calc_screen(self) -> list[str]:
|
||||
screen = super().calc_screen()
|
||||
if self.cmpltn_menu_visible:
|
||||
# We display the completions menu below the current prompt
|
||||
ly = self.lxy[1] + 1
|
||||
screen[ly:ly] = self.cmpltn_menu
|
||||
# If we're not in the middle of multiline edit, don't append to screeninfo
|
||||
# since that screws up the position calculation in pos2xy function.
|
||||
# This is a hack to prevent the cursor jumping
|
||||
# into the completions menu when pressing left or down arrow.
|
||||
if self.pos != len(self.buffer):
|
||||
self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu)
|
||||
return screen
|
||||
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.cmpltn_reset()
|
||||
|
||||
def cmpltn_reset(self) -> None:
|
||||
self.cmpltn_menu = []
|
||||
self.cmpltn_menu_visible = False
|
||||
self.cmpltn_message_visible = False
|
||||
self.cmpltn_menu_end = 0
|
||||
self.cmpltn_menu_choices = []
|
||||
|
||||
def get_stem(self) -> str:
|
||||
st = self.syntax_table
|
||||
SW = reader.SYNTAX_WORD
|
||||
b = self.buffer
|
||||
p = self.pos - 1
|
||||
while p >= 0 and st.get(b[p], SW) == SW:
|
||||
p -= 1
|
||||
return ''.join(b[p+1:self.pos])
|
||||
|
||||
def get_completions(self, stem: str) -> list[str]:
|
||||
return []
|
||||
213
Lib/_pyrepl/console.py
vendored
Normal file
213
Lib/_pyrepl/console.py
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
# Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import _colorize # type: ignore[import-not-found]
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import ast
|
||||
import code
|
||||
from dataclasses import dataclass, field
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import IO
|
||||
from typing import Callable
|
||||
|
||||
|
||||
@dataclass
|
||||
class Event:
|
||||
evt: str
|
||||
data: str
|
||||
raw: bytes = b""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Console(ABC):
|
||||
posxy: tuple[int, int]
|
||||
screen: list[str] = field(default_factory=list)
|
||||
height: int = 25
|
||||
width: int = 80
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
f_in: IO[bytes] | int = 0,
|
||||
f_out: IO[bytes] | int = 1,
|
||||
term: str = "",
|
||||
encoding: str = "",
|
||||
):
|
||||
self.encoding = encoding or sys.getdefaultencoding()
|
||||
|
||||
if isinstance(f_in, int):
|
||||
self.input_fd = f_in
|
||||
else:
|
||||
self.input_fd = f_in.fileno()
|
||||
|
||||
if isinstance(f_out, int):
|
||||
self.output_fd = f_out
|
||||
else:
|
||||
self.output_fd = f_out.fileno()
|
||||
|
||||
@abstractmethod
|
||||
def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def prepare(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def restore(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def move_cursor(self, x: int, y: int) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def set_cursor_vis(self, visible: bool) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def getheightwidth(self) -> tuple[int, int]:
|
||||
"""Return (height, width) where height and width are the height
|
||||
and width of the terminal window in characters."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def get_event(self, block: bool = True) -> Event | None:
|
||||
"""Return an Event instance. Returns None if |block| is false
|
||||
and there is no event pending, otherwise waits for the
|
||||
completion of an event."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def push_char(self, char: int | bytes) -> None:
|
||||
"""
|
||||
Push a character to the console event queue.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def beep(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
def clear(self) -> None:
|
||||
"""Wipe the screen"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def finish(self) -> None:
|
||||
"""Move the cursor to the end of the display and otherwise get
|
||||
ready for end. XXX could be merged with restore? Hmm."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def flushoutput(self) -> None:
|
||||
"""Flush all output to the screen (assuming there's some
|
||||
buffering going on somewhere)."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def forgetinput(self) -> None:
|
||||
"""Forget all pending, but not yet processed input."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def getpending(self) -> Event:
|
||||
"""Return the characters that have been typed but not yet
|
||||
processed."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def wait(self, timeout: float | None) -> bool:
|
||||
"""Wait for an event. The return value is True if an event is
|
||||
available, False if the timeout has been reached. If timeout is
|
||||
None, wait forever. The timeout is in milliseconds."""
|
||||
...
|
||||
|
||||
@property
|
||||
def input_hook(self) -> Callable[[], int] | None:
|
||||
"""Returns the current input hook."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def repaint(self) -> None: ...
|
||||
|
||||
|
||||
class InteractiveColoredConsole(code.InteractiveConsole):
|
||||
def __init__(
|
||||
self,
|
||||
locals: dict[str, object] | None = None,
|
||||
filename: str = "<console>",
|
||||
*,
|
||||
local_exit: bool = False,
|
||||
) -> None:
|
||||
super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg]
|
||||
self.can_colorize = _colorize.can_colorize()
|
||||
|
||||
def showsyntaxerror(self, filename=None, **kwargs):
|
||||
super().showsyntaxerror(filename=filename, **kwargs)
|
||||
|
||||
def _excepthook(self, typ, value, tb):
|
||||
import traceback
|
||||
lines = traceback.format_exception(
|
||||
typ, value, tb,
|
||||
colorize=self.can_colorize,
|
||||
limit=traceback.BUILTIN_EXCEPTION_LIMIT)
|
||||
self.write(''.join(lines))
|
||||
|
||||
def runsource(self, source, filename="<input>", symbol="single"):
|
||||
try:
|
||||
tree = self.compile.compiler(
|
||||
source,
|
||||
filename,
|
||||
"exec",
|
||||
ast.PyCF_ONLY_AST,
|
||||
incomplete_input=False,
|
||||
)
|
||||
except (SyntaxError, OverflowError, ValueError):
|
||||
self.showsyntaxerror(filename, source=source)
|
||||
return False
|
||||
if tree.body:
|
||||
*_, last_stmt = tree.body
|
||||
for stmt in tree.body:
|
||||
wrapper = ast.Interactive if stmt is last_stmt else ast.Module
|
||||
the_symbol = symbol if stmt is last_stmt else "exec"
|
||||
item = wrapper([stmt])
|
||||
try:
|
||||
code = self.compile.compiler(item, filename, the_symbol)
|
||||
except SyntaxError as e:
|
||||
if e.args[0] == "'await' outside function":
|
||||
python = os.path.basename(sys.executable)
|
||||
e.add_note(
|
||||
f"Try the asyncio REPL ({python} -m asyncio) to use"
|
||||
f" top-level 'await' and run background asyncio tasks."
|
||||
)
|
||||
self.showsyntaxerror(filename, source=source)
|
||||
return False
|
||||
except (OverflowError, ValueError):
|
||||
self.showsyntaxerror(filename, source=source)
|
||||
return False
|
||||
|
||||
if code is None:
|
||||
return True
|
||||
|
||||
self.runcode(code)
|
||||
return False
|
||||
33
Lib/_pyrepl/curses.py
vendored
Normal file
33
Lib/_pyrepl/curses.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
try:
|
||||
import _curses
|
||||
except ImportError:
|
||||
try:
|
||||
import curses as _curses # type: ignore[no-redef]
|
||||
except ImportError:
|
||||
from . import _minimal_curses as _curses # type: ignore[no-redef]
|
||||
|
||||
setupterm = _curses.setupterm
|
||||
tigetstr = _curses.tigetstr
|
||||
tparm = _curses.tparm
|
||||
error = _curses.error
|
||||
76
Lib/_pyrepl/fancy_termios.py
vendored
Normal file
76
Lib/_pyrepl/fancy_termios.py
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
# Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import termios
|
||||
|
||||
|
||||
class TermState:
|
||||
def __init__(self, tuples):
|
||||
(
|
||||
self.iflag,
|
||||
self.oflag,
|
||||
self.cflag,
|
||||
self.lflag,
|
||||
self.ispeed,
|
||||
self.ospeed,
|
||||
self.cc,
|
||||
) = tuples
|
||||
|
||||
def as_list(self):
|
||||
return [
|
||||
self.iflag,
|
||||
self.oflag,
|
||||
self.cflag,
|
||||
self.lflag,
|
||||
self.ispeed,
|
||||
self.ospeed,
|
||||
# Always return a copy of the control characters list to ensure
|
||||
# there are not any additional references to self.cc
|
||||
self.cc[:],
|
||||
]
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self.as_list())
|
||||
|
||||
|
||||
def tcgetattr(fd):
|
||||
return TermState(termios.tcgetattr(fd))
|
||||
|
||||
|
||||
def tcsetattr(fd, when, attrs):
|
||||
termios.tcsetattr(fd, when, attrs.as_list())
|
||||
|
||||
|
||||
class Term(TermState):
|
||||
TS__init__ = TermState.__init__
|
||||
|
||||
def __init__(self, fd=0):
|
||||
self.TS__init__(termios.tcgetattr(fd))
|
||||
self.fd = fd
|
||||
self.stack = []
|
||||
|
||||
def save(self):
|
||||
self.stack.append(self.as_list())
|
||||
|
||||
def set(self, when=termios.TCSANOW):
|
||||
termios.tcsetattr(self.fd, when, self.as_list())
|
||||
|
||||
def restore(self):
|
||||
self.TS__init__(self.stack.pop())
|
||||
self.set()
|
||||
419
Lib/_pyrepl/historical_reader.py
vendored
Normal file
419
Lib/_pyrepl/historical_reader.py
vendored
Normal file
@@ -0,0 +1,419 @@
|
||||
# Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from . import commands, input
|
||||
from .reader import Reader
|
||||
|
||||
|
||||
if False:
|
||||
from .types import SimpleContextManager, KeySpec, CommandName
|
||||
|
||||
|
||||
isearch_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple(
|
||||
[("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) != "\\"]
|
||||
+ [(c, "isearch-add-character") for c in map(chr, range(32, 127)) if c != "\\"]
|
||||
+ [
|
||||
("\\%03o" % c, "isearch-add-character")
|
||||
for c in range(256)
|
||||
if chr(c).isalpha() and chr(c) != "\\"
|
||||
]
|
||||
+ [
|
||||
("\\\\", "self-insert"),
|
||||
(r"\C-r", "isearch-backwards"),
|
||||
(r"\C-s", "isearch-forwards"),
|
||||
(r"\C-c", "isearch-cancel"),
|
||||
(r"\C-g", "isearch-cancel"),
|
||||
(r"\<backspace>", "isearch-backspace"),
|
||||
]
|
||||
)
|
||||
|
||||
ISEARCH_DIRECTION_NONE = ""
|
||||
ISEARCH_DIRECTION_BACKWARDS = "r"
|
||||
ISEARCH_DIRECTION_FORWARDS = "f"
|
||||
|
||||
|
||||
class next_history(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
if r.historyi == len(r.history):
|
||||
r.error("end of history list")
|
||||
return
|
||||
r.select_item(r.historyi + 1)
|
||||
|
||||
|
||||
class previous_history(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
if r.historyi == 0:
|
||||
r.error("start of history list")
|
||||
return
|
||||
r.select_item(r.historyi - 1)
|
||||
|
||||
|
||||
class history_search_backward(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.search_next(forwards=False)
|
||||
|
||||
|
||||
class history_search_forward(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.search_next(forwards=True)
|
||||
|
||||
|
||||
class restore_history(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
if r.historyi != len(r.history):
|
||||
if r.get_unicode() != r.history[r.historyi]:
|
||||
r.buffer = list(r.history[r.historyi])
|
||||
r.pos = len(r.buffer)
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class first_history(commands.Command):
|
||||
def do(self) -> None:
|
||||
self.reader.select_item(0)
|
||||
|
||||
|
||||
class last_history(commands.Command):
|
||||
def do(self) -> None:
|
||||
self.reader.select_item(len(self.reader.history))
|
||||
|
||||
|
||||
class operate_and_get_next(commands.FinishCommand):
|
||||
def do(self) -> None:
|
||||
self.reader.next_history = self.reader.historyi + 1
|
||||
|
||||
|
||||
class yank_arg(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
if r.last_command is self.__class__:
|
||||
r.yank_arg_i += 1
|
||||
else:
|
||||
r.yank_arg_i = 0
|
||||
if r.historyi < r.yank_arg_i:
|
||||
r.error("beginning of history list")
|
||||
return
|
||||
a = r.get_arg(-1)
|
||||
# XXX how to split?
|
||||
words = r.get_item(r.historyi - r.yank_arg_i - 1).split()
|
||||
if a < -len(words) or a >= len(words):
|
||||
r.error("no such arg")
|
||||
return
|
||||
w = words[a]
|
||||
b = r.buffer
|
||||
if r.yank_arg_i > 0:
|
||||
o = len(r.yank_arg_yanked)
|
||||
else:
|
||||
o = 0
|
||||
b[r.pos - o : r.pos] = list(w)
|
||||
r.yank_arg_yanked = w
|
||||
r.pos += len(w) - o
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class forward_history_isearch(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.isearch_direction = ISEARCH_DIRECTION_FORWARDS
|
||||
r.isearch_start = r.historyi, r.pos
|
||||
r.isearch_term = ""
|
||||
r.dirty = True
|
||||
r.push_input_trans(r.isearch_trans)
|
||||
|
||||
|
||||
class reverse_history_isearch(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS
|
||||
r.dirty = True
|
||||
r.isearch_term = ""
|
||||
r.push_input_trans(r.isearch_trans)
|
||||
r.isearch_start = r.historyi, r.pos
|
||||
|
||||
|
||||
class isearch_cancel(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.isearch_direction = ISEARCH_DIRECTION_NONE
|
||||
r.pop_input_trans()
|
||||
r.select_item(r.isearch_start[0])
|
||||
r.pos = r.isearch_start[1]
|
||||
r.dirty = True
|
||||
|
||||
|
||||
class isearch_add_character(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
r.isearch_term += self.event[-1]
|
||||
r.dirty = True
|
||||
p = r.pos + len(r.isearch_term) - 1
|
||||
if b[p : p + 1] != [r.isearch_term[-1]]:
|
||||
r.isearch_next()
|
||||
|
||||
|
||||
class isearch_backspace(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
if len(r.isearch_term) > 0:
|
||||
r.isearch_term = r.isearch_term[:-1]
|
||||
r.dirty = True
|
||||
else:
|
||||
r.error("nothing to rubout")
|
||||
|
||||
|
||||
class isearch_forwards(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.isearch_direction = ISEARCH_DIRECTION_FORWARDS
|
||||
r.isearch_next()
|
||||
|
||||
|
||||
class isearch_backwards(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS
|
||||
r.isearch_next()
|
||||
|
||||
|
||||
class isearch_end(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
r.isearch_direction = ISEARCH_DIRECTION_NONE
|
||||
r.console.forgetinput()
|
||||
r.pop_input_trans()
|
||||
r.dirty = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoricalReader(Reader):
|
||||
"""Adds history support (with incremental history searching) to the
|
||||
Reader class.
|
||||
"""
|
||||
|
||||
history: list[str] = field(default_factory=list)
|
||||
historyi: int = 0
|
||||
next_history: int | None = None
|
||||
transient_history: dict[int, str] = field(default_factory=dict)
|
||||
isearch_term: str = ""
|
||||
isearch_direction: str = ISEARCH_DIRECTION_NONE
|
||||
isearch_start: tuple[int, int] = field(init=False)
|
||||
isearch_trans: input.KeymapTranslator = field(init=False)
|
||||
yank_arg_i: int = 0
|
||||
yank_arg_yanked: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
for c in [
|
||||
next_history,
|
||||
previous_history,
|
||||
restore_history,
|
||||
first_history,
|
||||
last_history,
|
||||
yank_arg,
|
||||
forward_history_isearch,
|
||||
reverse_history_isearch,
|
||||
isearch_end,
|
||||
isearch_add_character,
|
||||
isearch_cancel,
|
||||
isearch_add_character,
|
||||
isearch_backspace,
|
||||
isearch_forwards,
|
||||
isearch_backwards,
|
||||
operate_and_get_next,
|
||||
history_search_backward,
|
||||
history_search_forward,
|
||||
]:
|
||||
self.commands[c.__name__] = c
|
||||
self.commands[c.__name__.replace("_", "-")] = c
|
||||
self.isearch_start = self.historyi, self.pos
|
||||
self.isearch_trans = input.KeymapTranslator(
|
||||
isearch_keymap, invalid_cls=isearch_end, character_cls=isearch_add_character
|
||||
)
|
||||
|
||||
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
|
||||
return super().collect_keymap() + (
|
||||
(r"\C-n", "next-history"),
|
||||
(r"\C-p", "previous-history"),
|
||||
(r"\C-o", "operate-and-get-next"),
|
||||
(r"\C-r", "reverse-history-isearch"),
|
||||
(r"\C-s", "forward-history-isearch"),
|
||||
(r"\M-r", "restore-history"),
|
||||
(r"\M-.", "yank-arg"),
|
||||
(r"\<page down>", "history-search-forward"),
|
||||
(r"\x1b[6~", "history-search-forward"),
|
||||
(r"\<page up>", "history-search-backward"),
|
||||
(r"\x1b[5~", "history-search-backward"),
|
||||
)
|
||||
|
||||
def select_item(self, i: int) -> None:
|
||||
self.transient_history[self.historyi] = self.get_unicode()
|
||||
buf = self.transient_history.get(i)
|
||||
if buf is None:
|
||||
buf = self.history[i].rstrip()
|
||||
self.buffer = list(buf)
|
||||
self.historyi = i
|
||||
self.pos = len(self.buffer)
|
||||
self.dirty = True
|
||||
self.last_refresh_cache.invalidated = True
|
||||
|
||||
def get_item(self, i: int) -> str:
|
||||
if i != len(self.history):
|
||||
return self.transient_history.get(i, self.history[i])
|
||||
else:
|
||||
return self.transient_history.get(i, self.get_unicode())
|
||||
|
||||
@contextmanager
|
||||
def suspend(self) -> SimpleContextManager:
|
||||
with super().suspend(), self.suspend_history():
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def suspend_history(self) -> SimpleContextManager:
|
||||
try:
|
||||
old_history = self.history[:]
|
||||
del self.history[:]
|
||||
yield
|
||||
finally:
|
||||
self.history[:] = old_history
|
||||
|
||||
def prepare(self) -> None:
|
||||
super().prepare()
|
||||
try:
|
||||
self.transient_history = {}
|
||||
if self.next_history is not None and self.next_history < len(self.history):
|
||||
self.historyi = self.next_history
|
||||
self.buffer[:] = list(self.history[self.next_history])
|
||||
self.pos = len(self.buffer)
|
||||
self.transient_history[len(self.history)] = ""
|
||||
else:
|
||||
self.historyi = len(self.history)
|
||||
self.next_history = None
|
||||
except:
|
||||
self.restore()
|
||||
raise
|
||||
|
||||
def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
|
||||
if cursor_on_line and self.isearch_direction != ISEARCH_DIRECTION_NONE:
|
||||
d = "rf"[self.isearch_direction == ISEARCH_DIRECTION_FORWARDS]
|
||||
return "(%s-search `%s') " % (d, self.isearch_term)
|
||||
else:
|
||||
return super().get_prompt(lineno, cursor_on_line)
|
||||
|
||||
def search_next(self, *, forwards: bool) -> None:
|
||||
"""Search history for the current line contents up to the cursor.
|
||||
|
||||
Selects the first item found. If nothing is under the cursor, any next
|
||||
item in history is selected.
|
||||
"""
|
||||
pos = self.pos
|
||||
s = self.get_unicode()
|
||||
history_index = self.historyi
|
||||
|
||||
# In multiline contexts, we're only interested in the current line.
|
||||
nl_index = s.rfind('\n', 0, pos)
|
||||
prefix = s[nl_index + 1:pos]
|
||||
pos = len(prefix)
|
||||
|
||||
match_prefix = len(prefix)
|
||||
len_item = 0
|
||||
if history_index < len(self.history):
|
||||
len_item = len(self.get_item(history_index))
|
||||
if len_item and pos == len_item:
|
||||
match_prefix = False
|
||||
elif not pos:
|
||||
match_prefix = False
|
||||
|
||||
while 1:
|
||||
if forwards:
|
||||
out_of_bounds = history_index >= len(self.history) - 1
|
||||
else:
|
||||
out_of_bounds = history_index == 0
|
||||
if out_of_bounds:
|
||||
if forwards and not match_prefix:
|
||||
self.pos = 0
|
||||
self.buffer = []
|
||||
self.dirty = True
|
||||
else:
|
||||
self.error("not found")
|
||||
return
|
||||
|
||||
history_index += 1 if forwards else -1
|
||||
s = self.get_item(history_index)
|
||||
|
||||
if not match_prefix:
|
||||
self.select_item(history_index)
|
||||
return
|
||||
|
||||
len_acc = 0
|
||||
for i, line in enumerate(s.splitlines(keepends=True)):
|
||||
if line.startswith(prefix):
|
||||
self.select_item(history_index)
|
||||
self.pos = pos + len_acc
|
||||
return
|
||||
len_acc += len(line)
|
||||
|
||||
def isearch_next(self) -> None:
|
||||
st = self.isearch_term
|
||||
p = self.pos
|
||||
i = self.historyi
|
||||
s = self.get_unicode()
|
||||
forwards = self.isearch_direction == ISEARCH_DIRECTION_FORWARDS
|
||||
while 1:
|
||||
if forwards:
|
||||
p = s.find(st, p + 1)
|
||||
else:
|
||||
p = s.rfind(st, 0, p + len(st) - 1)
|
||||
if p != -1:
|
||||
self.select_item(i)
|
||||
self.pos = p
|
||||
return
|
||||
elif (forwards and i >= len(self.history) - 1) or (not forwards and i == 0):
|
||||
self.error("not found")
|
||||
return
|
||||
else:
|
||||
if forwards:
|
||||
i += 1
|
||||
s = self.get_item(i)
|
||||
p = -1
|
||||
else:
|
||||
i -= 1
|
||||
s = self.get_item(i)
|
||||
p = len(s)
|
||||
|
||||
def finish(self) -> None:
|
||||
super().finish()
|
||||
ret = self.get_unicode()
|
||||
for i, t in self.transient_history.items():
|
||||
if i < len(self.history) and i != self.historyi:
|
||||
self.history[i] = t
|
||||
if ret and should_auto_add_history:
|
||||
self.history.append(ret)
|
||||
|
||||
|
||||
should_auto_add_history = True
|
||||
114
Lib/_pyrepl/input.py
vendored
Normal file
114
Lib/_pyrepl/input.py
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
# Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
# (naming modules after builtin functions is not such a hot idea...)
|
||||
|
||||
# an KeyTrans instance translates Event objects into Command objects
|
||||
|
||||
# hmm, at what level do we want [C-i] and [tab] to be equivalent?
|
||||
# [meta-a] and [esc a]? obviously, these are going to be equivalent
|
||||
# for the UnixConsole, but should they be for PygameConsole?
|
||||
|
||||
# it would in any situation seem to be a bad idea to bind, say, [tab]
|
||||
# and [C-i] to *different* things... but should binding one bind the
|
||||
# other?
|
||||
|
||||
# executive, temporary decision: [tab] and [C-i] are distinct, but
|
||||
# [meta-key] is identified with [esc key]. We demand that any console
|
||||
# class does quite a lot towards emulating a unix terminal.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import unicodedata
|
||||
from collections import deque
|
||||
|
||||
|
||||
# types
|
||||
if False:
|
||||
from .types import EventTuple
|
||||
|
||||
|
||||
class InputTranslator(ABC):
|
||||
@abstractmethod
|
||||
def push(self, evt: EventTuple) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self) -> EventTuple | None:
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def empty(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class KeymapTranslator(InputTranslator):
|
||||
def __init__(self, keymap, verbose=False, invalid_cls=None, character_cls=None):
|
||||
self.verbose = verbose
|
||||
from .keymap import compile_keymap, parse_keys
|
||||
|
||||
self.keymap = keymap
|
||||
self.invalid_cls = invalid_cls
|
||||
self.character_cls = character_cls
|
||||
d = {}
|
||||
for keyspec, command in keymap:
|
||||
keyseq = tuple(parse_keys(keyspec))
|
||||
d[keyseq] = command
|
||||
if self.verbose:
|
||||
print(d)
|
||||
self.k = self.ck = compile_keymap(d, ())
|
||||
self.results = deque()
|
||||
self.stack = []
|
||||
|
||||
def push(self, evt):
|
||||
if self.verbose:
|
||||
print("pushed", evt.data, end="")
|
||||
key = evt.data
|
||||
d = self.k.get(key)
|
||||
if isinstance(d, dict):
|
||||
if self.verbose:
|
||||
print("transition")
|
||||
self.stack.append(key)
|
||||
self.k = d
|
||||
else:
|
||||
if d is None:
|
||||
if self.verbose:
|
||||
print("invalid")
|
||||
if self.stack or len(key) > 1 or unicodedata.category(key) == "C":
|
||||
self.results.append((self.invalid_cls, self.stack + [key]))
|
||||
else:
|
||||
# small optimization:
|
||||
self.k[key] = self.character_cls
|
||||
self.results.append((self.character_cls, [key]))
|
||||
else:
|
||||
if self.verbose:
|
||||
print("matched", d)
|
||||
self.results.append((d, self.stack + [key]))
|
||||
self.stack = []
|
||||
self.k = self.ck
|
||||
|
||||
def get(self):
|
||||
if self.results:
|
||||
return self.results.popleft()
|
||||
else:
|
||||
return None
|
||||
|
||||
def empty(self) -> bool:
|
||||
return not self.results
|
||||
213
Lib/_pyrepl/keymap.py
vendored
Normal file
213
Lib/_pyrepl/keymap.py
vendored
Normal file
@@ -0,0 +1,213 @@
|
||||
# Copyright 2000-2008 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
"""
|
||||
Keymap contains functions for parsing keyspecs and turning keyspecs into
|
||||
appropriate sequences.
|
||||
|
||||
A keyspec is a string representing a sequence of key presses that can
|
||||
be bound to a command. All characters other than the backslash represent
|
||||
themselves. In the traditional manner, a backslash introduces an escape
|
||||
sequence.
|
||||
|
||||
pyrepl uses its own keyspec format that is meant to be a strict superset of
|
||||
readline's KEYSEQ format. This means that if a spec is found that readline
|
||||
accepts that this doesn't, it should be logged as a bug. Note that this means
|
||||
we're using the `\\C-o' style of readline's keyspec, not the `Control-o' sort.
|
||||
|
||||
The extension to readline is that the sequence \\<KEY> denotes the
|
||||
sequence of characters produced by hitting KEY.
|
||||
|
||||
Examples:
|
||||
`a' - what you get when you hit the `a' key
|
||||
`\\EOA' - Escape - O - A (up, on my terminal)
|
||||
`\\<UP>' - the up arrow key
|
||||
`\\<up>' - ditto (keynames are case-insensitive)
|
||||
`\\C-o', `\\c-o' - control-o
|
||||
`\\M-.' - meta-period
|
||||
`\\E.' - ditto (that's how meta works for pyrepl)
|
||||
`\\<tab>', `\\<TAB>', `\\t', `\\011', '\\x09', '\\X09', '\\C-i', '\\C-I'
|
||||
- all of these are the tab character.
|
||||
"""
|
||||
|
||||
_escapes = {
|
||||
"\\": "\\",
|
||||
"'": "'",
|
||||
'"': '"',
|
||||
"a": "\a",
|
||||
"b": "\b",
|
||||
"e": "\033",
|
||||
"f": "\f",
|
||||
"n": "\n",
|
||||
"r": "\r",
|
||||
"t": "\t",
|
||||
"v": "\v",
|
||||
}
|
||||
|
||||
_keynames = {
|
||||
"backspace": "backspace",
|
||||
"delete": "delete",
|
||||
"down": "down",
|
||||
"end": "end",
|
||||
"enter": "\r",
|
||||
"escape": "\033",
|
||||
"f1": "f1",
|
||||
"f2": "f2",
|
||||
"f3": "f3",
|
||||
"f4": "f4",
|
||||
"f5": "f5",
|
||||
"f6": "f6",
|
||||
"f7": "f7",
|
||||
"f8": "f8",
|
||||
"f9": "f9",
|
||||
"f10": "f10",
|
||||
"f11": "f11",
|
||||
"f12": "f12",
|
||||
"f13": "f13",
|
||||
"f14": "f14",
|
||||
"f15": "f15",
|
||||
"f16": "f16",
|
||||
"f17": "f17",
|
||||
"f18": "f18",
|
||||
"f19": "f19",
|
||||
"f20": "f20",
|
||||
"home": "home",
|
||||
"insert": "insert",
|
||||
"left": "left",
|
||||
"page down": "page down",
|
||||
"page up": "page up",
|
||||
"return": "\r",
|
||||
"right": "right",
|
||||
"space": " ",
|
||||
"tab": "\t",
|
||||
"up": "up",
|
||||
}
|
||||
|
||||
|
||||
class KeySpecError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def parse_keys(keys: str) -> list[str]:
|
||||
"""Parse keys in keyspec format to a sequence of keys."""
|
||||
s = 0
|
||||
r: list[str] = []
|
||||
while s < len(keys):
|
||||
k, s = _parse_single_key_sequence(keys, s)
|
||||
r.extend(k)
|
||||
return r
|
||||
|
||||
|
||||
def _parse_single_key_sequence(key: str, s: int) -> tuple[list[str], int]:
|
||||
ctrl = 0
|
||||
meta = 0
|
||||
ret = ""
|
||||
while not ret and s < len(key):
|
||||
if key[s] == "\\":
|
||||
c = key[s + 1].lower()
|
||||
if c in _escapes:
|
||||
ret = _escapes[c]
|
||||
s += 2
|
||||
elif c == "c":
|
||||
if key[s + 2] != "-":
|
||||
raise KeySpecError(
|
||||
"\\C must be followed by `-' (char %d of %s)"
|
||||
% (s + 2, repr(key))
|
||||
)
|
||||
if ctrl:
|
||||
raise KeySpecError(
|
||||
"doubled \\C- (char %d of %s)" % (s + 1, repr(key))
|
||||
)
|
||||
ctrl = 1
|
||||
s += 3
|
||||
elif c == "m":
|
||||
if key[s + 2] != "-":
|
||||
raise KeySpecError(
|
||||
"\\M must be followed by `-' (char %d of %s)"
|
||||
% (s + 2, repr(key))
|
||||
)
|
||||
if meta:
|
||||
raise KeySpecError(
|
||||
"doubled \\M- (char %d of %s)" % (s + 1, repr(key))
|
||||
)
|
||||
meta = 1
|
||||
s += 3
|
||||
elif c.isdigit():
|
||||
n = key[s + 1 : s + 4]
|
||||
ret = chr(int(n, 8))
|
||||
s += 4
|
||||
elif c == "x":
|
||||
n = key[s + 2 : s + 4]
|
||||
ret = chr(int(n, 16))
|
||||
s += 4
|
||||
elif c == "<":
|
||||
t = key.find(">", s)
|
||||
if t == -1:
|
||||
raise KeySpecError(
|
||||
"unterminated \\< starting at char %d of %s"
|
||||
% (s + 1, repr(key))
|
||||
)
|
||||
ret = key[s + 2 : t].lower()
|
||||
if ret not in _keynames:
|
||||
raise KeySpecError(
|
||||
"unrecognised keyname `%s' at char %d of %s"
|
||||
% (ret, s + 2, repr(key))
|
||||
)
|
||||
ret = _keynames[ret]
|
||||
s = t + 1
|
||||
else:
|
||||
raise KeySpecError(
|
||||
"unknown backslash escape %s at char %d of %s"
|
||||
% (repr(c), s + 2, repr(key))
|
||||
)
|
||||
else:
|
||||
ret = key[s]
|
||||
s += 1
|
||||
if ctrl:
|
||||
if len(ret) == 1:
|
||||
ret = chr(ord(ret) & 0x1F) # curses.ascii.ctrl()
|
||||
elif ret in {"left", "right"}:
|
||||
ret = f"ctrl {ret}"
|
||||
else:
|
||||
raise KeySpecError("\\C- followed by invalid key")
|
||||
|
||||
result = [ret], s
|
||||
if meta:
|
||||
result[0].insert(0, "\033")
|
||||
return result
|
||||
|
||||
|
||||
def compile_keymap(keymap, empty=b""):
|
||||
r = {}
|
||||
for key, value in keymap.items():
|
||||
if isinstance(key, bytes):
|
||||
first = key[:1]
|
||||
else:
|
||||
first = key[0]
|
||||
r.setdefault(first, {})[key[1:]] = value
|
||||
for key, value in r.items():
|
||||
if empty in value:
|
||||
if len(value) != 1:
|
||||
raise KeySpecError("key definitions for %s clash" % (value.values(),))
|
||||
else:
|
||||
r[key] = value[empty]
|
||||
else:
|
||||
r[key] = compile_keymap(value, empty)
|
||||
return r
|
||||
59
Lib/_pyrepl/main.py
vendored
Normal file
59
Lib/_pyrepl/main.py
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
CAN_USE_PYREPL: bool
|
||||
FAIL_REASON: str
|
||||
try:
|
||||
if sys.platform == "win32" and sys.getwindowsversion().build < 10586:
|
||||
raise RuntimeError("Windows 10 TH2 or later required")
|
||||
if not os.isatty(sys.stdin.fileno()):
|
||||
raise OSError(errno.ENOTTY, "tty required", "stdin")
|
||||
from .simple_interact import check
|
||||
if err := check():
|
||||
raise RuntimeError(err)
|
||||
except Exception as e:
|
||||
CAN_USE_PYREPL = False
|
||||
FAIL_REASON = f"warning: can't use pyrepl: {e}"
|
||||
else:
|
||||
CAN_USE_PYREPL = True
|
||||
FAIL_REASON = ""
|
||||
|
||||
|
||||
def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
|
||||
if not CAN_USE_PYREPL:
|
||||
if not os.getenv('PYTHON_BASIC_REPL') and FAIL_REASON:
|
||||
from .trace import trace
|
||||
trace(FAIL_REASON)
|
||||
print(FAIL_REASON, file=sys.stderr)
|
||||
return sys._baserepl()
|
||||
|
||||
if mainmodule:
|
||||
namespace = mainmodule.__dict__
|
||||
else:
|
||||
import __main__
|
||||
namespace = __main__.__dict__
|
||||
namespace.pop("__pyrepl_interactive_console", None)
|
||||
|
||||
# sys._baserepl() above does this internally, we do it here
|
||||
startup_path = os.getenv("PYTHONSTARTUP")
|
||||
if pythonstartup and startup_path:
|
||||
sys.audit("cpython.run_startup", startup_path)
|
||||
|
||||
import tokenize
|
||||
with tokenize.open(startup_path) as f:
|
||||
startup_code = compile(f.read(), startup_path, "exec")
|
||||
exec(startup_code, namespace)
|
||||
|
||||
# set sys.{ps1,ps2} just before invoking the interactive interpreter. This
|
||||
# mimics what CPython does in pythonrun.c
|
||||
if not hasattr(sys, "ps1"):
|
||||
sys.ps1 = ">>> "
|
||||
if not hasattr(sys, "ps2"):
|
||||
sys.ps2 = "... "
|
||||
|
||||
from .console import InteractiveColoredConsole
|
||||
from .simple_interact import run_multiline_interactive_console
|
||||
console = InteractiveColoredConsole(namespace, filename="<stdin>")
|
||||
run_multiline_interactive_console(console)
|
||||
24
Lib/_pyrepl/mypy.ini
vendored
Normal file
24
Lib/_pyrepl/mypy.ini
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Config file for running mypy on _pyrepl.
|
||||
# Run mypy by invoking `mypy --config-file Lib/_pyrepl/mypy.ini`
|
||||
# on the command-line from the repo root
|
||||
|
||||
[mypy]
|
||||
files = Lib/_pyrepl
|
||||
explicit_package_bases = True
|
||||
python_version = 3.12
|
||||
platform = linux
|
||||
pretty = True
|
||||
|
||||
# Enable most stricter settings
|
||||
enable_error_code = ignore-without-code,redundant-expr
|
||||
strict = True
|
||||
|
||||
# Various stricter settings that we can't yet enable
|
||||
# Try to enable these in the following order:
|
||||
disallow_untyped_calls = False
|
||||
disallow_untyped_defs = False
|
||||
check_untyped_defs = False
|
||||
|
||||
# Various internal modules that typeshed deliberately doesn't have stubs for:
|
||||
[mypy-_abc.*,_opcode.*,_overlapped.*,_testcapi.*,_testinternalcapi.*,test.*]
|
||||
ignore_missing_imports = True
|
||||
175
Lib/_pyrepl/pager.py
vendored
Normal file
175
Lib/_pyrepl/pager.py
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
# types
|
||||
if False:
|
||||
from typing import Protocol
|
||||
class Pager(Protocol):
|
||||
def __call__(self, text: str, title: str = "") -> None:
|
||||
...
|
||||
|
||||
|
||||
def get_pager() -> Pager:
|
||||
"""Decide what method to use for paging through text."""
|
||||
if not hasattr(sys.stdin, "isatty"):
|
||||
return plain_pager
|
||||
if not hasattr(sys.stdout, "isatty"):
|
||||
return plain_pager
|
||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||
return plain_pager
|
||||
if sys.platform == "emscripten":
|
||||
return plain_pager
|
||||
use_pager = os.environ.get('MANPAGER') or os.environ.get('PAGER')
|
||||
if use_pager:
|
||||
if sys.platform == 'win32': # pipes completely broken in Windows
|
||||
return lambda text, title='': tempfile_pager(plain(text), use_pager)
|
||||
elif os.environ.get('TERM') in ('dumb', 'emacs'):
|
||||
return lambda text, title='': pipe_pager(plain(text), use_pager, title)
|
||||
else:
|
||||
return lambda text, title='': pipe_pager(text, use_pager, title)
|
||||
if os.environ.get('TERM') in ('dumb', 'emacs'):
|
||||
return plain_pager
|
||||
if sys.platform == 'win32':
|
||||
return lambda text, title='': tempfile_pager(plain(text), 'more <')
|
||||
if hasattr(os, 'system') and os.system('(pager) 2>/dev/null') == 0:
|
||||
return lambda text, title='': pipe_pager(text, 'pager', title)
|
||||
if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0:
|
||||
return lambda text, title='': pipe_pager(text, 'less', title)
|
||||
|
||||
import tempfile
|
||||
(fd, filename) = tempfile.mkstemp()
|
||||
os.close(fd)
|
||||
try:
|
||||
if hasattr(os, 'system') and os.system('more "%s"' % filename) == 0:
|
||||
return lambda text, title='': pipe_pager(text, 'more', title)
|
||||
else:
|
||||
return tty_pager
|
||||
finally:
|
||||
os.unlink(filename)
|
||||
|
||||
|
||||
def escape_stdout(text: str) -> str:
|
||||
# Escape non-encodable characters to avoid encoding errors later
|
||||
encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8'
|
||||
return text.encode(encoding, 'backslashreplace').decode(encoding)
|
||||
|
||||
|
||||
def escape_less(s: str) -> str:
|
||||
return re.sub(r'([?:.%\\])', r'\\\1', s)
|
||||
|
||||
|
||||
def plain(text: str) -> str:
|
||||
"""Remove boldface formatting from text."""
|
||||
return re.sub('.\b', '', text)
|
||||
|
||||
|
||||
def tty_pager(text: str, title: str = '') -> None:
|
||||
"""Page through text on a text terminal."""
|
||||
lines = plain(escape_stdout(text)).split('\n')
|
||||
has_tty = False
|
||||
try:
|
||||
import tty
|
||||
import termios
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
tty.setcbreak(fd)
|
||||
has_tty = True
|
||||
|
||||
def getchar() -> str:
|
||||
return sys.stdin.read(1)
|
||||
|
||||
except (ImportError, AttributeError, io.UnsupportedOperation):
|
||||
def getchar() -> str:
|
||||
return sys.stdin.readline()[:-1][:1]
|
||||
|
||||
try:
|
||||
try:
|
||||
h = int(os.environ.get('LINES', 0))
|
||||
except ValueError:
|
||||
h = 0
|
||||
if h <= 1:
|
||||
h = 25
|
||||
r = inc = h - 1
|
||||
sys.stdout.write('\n'.join(lines[:inc]) + '\n')
|
||||
while lines[r:]:
|
||||
sys.stdout.write('-- more --')
|
||||
sys.stdout.flush()
|
||||
c = getchar()
|
||||
|
||||
if c in ('q', 'Q'):
|
||||
sys.stdout.write('\r \r')
|
||||
break
|
||||
elif c in ('\r', '\n'):
|
||||
sys.stdout.write('\r \r' + lines[r] + '\n')
|
||||
r = r + 1
|
||||
continue
|
||||
if c in ('b', 'B', '\x1b'):
|
||||
r = r - inc - inc
|
||||
if r < 0: r = 0
|
||||
sys.stdout.write('\n' + '\n'.join(lines[r:r+inc]) + '\n')
|
||||
r = r + inc
|
||||
|
||||
finally:
|
||||
if has_tty:
|
||||
termios.tcsetattr(fd, termios.TCSAFLUSH, old)
|
||||
|
||||
|
||||
def plain_pager(text: str, title: str = '') -> None:
|
||||
"""Simply print unformatted text. This is the ultimate fallback."""
|
||||
sys.stdout.write(plain(escape_stdout(text)))
|
||||
|
||||
|
||||
def pipe_pager(text: str, cmd: str, title: str = '') -> None:
|
||||
"""Page through text by feeding it to another program."""
|
||||
import subprocess
|
||||
env = os.environ.copy()
|
||||
if title:
|
||||
title += ' '
|
||||
esc_title = escape_less(title)
|
||||
prompt_string = (
|
||||
f' {esc_title}' +
|
||||
'?ltline %lt?L/%L.'
|
||||
':byte %bB?s/%s.'
|
||||
'.'
|
||||
'?e (END):?pB %pB\\%..'
|
||||
' (press h for help or q to quit)')
|
||||
env['LESS'] = '-RmPm{0}$PM{0}$'.format(prompt_string)
|
||||
proc = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
|
||||
errors='backslashreplace', env=env)
|
||||
assert proc.stdin is not None
|
||||
try:
|
||||
with proc.stdin as pipe:
|
||||
try:
|
||||
pipe.write(text)
|
||||
except KeyboardInterrupt:
|
||||
# We've hereby abandoned whatever text hasn't been written,
|
||||
# but the pager is still in control of the terminal.
|
||||
pass
|
||||
except OSError:
|
||||
pass # Ignore broken pipes caused by quitting the pager program.
|
||||
while True:
|
||||
try:
|
||||
proc.wait()
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
# Ignore ctl-c like the pager itself does. Otherwise the pager is
|
||||
# left running and the terminal is in raw mode and unusable.
|
||||
pass
|
||||
|
||||
|
||||
def tempfile_pager(text: str, cmd: str, title: str = '') -> None:
|
||||
"""Page through text by invoking a program on a temporary file."""
|
||||
import tempfile
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
filename = os.path.join(tempdir, 'pydoc.out')
|
||||
with open(filename, 'w', errors='backslashreplace',
|
||||
encoding=os.device_encoding(0) if
|
||||
sys.platform == 'win32' else None
|
||||
) as file:
|
||||
file.write(text)
|
||||
os.system(cmd + ' "' + filename + '"')
|
||||
816
Lib/_pyrepl/reader.py
vendored
Normal file
816
Lib/_pyrepl/reader.py
vendored
Normal file
@@ -0,0 +1,816 @@
|
||||
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Antonio Cuni
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field, fields
|
||||
import unicodedata
|
||||
from _colorize import can_colorize, ANSIColors # type: ignore[import-not-found]
|
||||
|
||||
|
||||
from . import commands, console, input
|
||||
from .utils import ANSI_ESCAPE_SEQUENCE, wlen, str_width
|
||||
from .trace import trace
|
||||
|
||||
|
||||
# types
|
||||
Command = commands.Command
|
||||
from .types import Callback, SimpleContextManager, KeySpec, CommandName
|
||||
|
||||
|
||||
def disp_str(buffer: str) -> tuple[str, list[int]]:
|
||||
"""disp_str(buffer:string) -> (string, [int])
|
||||
|
||||
Return the string that should be the printed representation of
|
||||
|buffer| and a list detailing where the characters of |buffer|
|
||||
get used up. E.g.:
|
||||
|
||||
>>> disp_str(chr(3))
|
||||
('^C', [1, 0])
|
||||
|
||||
"""
|
||||
b: list[int] = []
|
||||
s: list[str] = []
|
||||
for c in buffer:
|
||||
if c == '\x1a':
|
||||
s.append(c)
|
||||
b.append(2)
|
||||
elif ord(c) < 128:
|
||||
s.append(c)
|
||||
b.append(1)
|
||||
elif unicodedata.category(c).startswith("C"):
|
||||
c = r"\u%04x" % ord(c)
|
||||
s.append(c)
|
||||
b.extend([0] * (len(c) - 1))
|
||||
else:
|
||||
s.append(c)
|
||||
b.append(str_width(c))
|
||||
return "".join(s), b
|
||||
|
||||
|
||||
# syntax classes:
|
||||
|
||||
SYNTAX_WHITESPACE, SYNTAX_WORD, SYNTAX_SYMBOL = range(3)
|
||||
|
||||
|
||||
def make_default_syntax_table() -> dict[str, int]:
|
||||
# XXX perhaps should use some unicodedata here?
|
||||
st: dict[str, int] = {}
|
||||
for c in map(chr, range(256)):
|
||||
st[c] = SYNTAX_SYMBOL
|
||||
for c in [a for a in map(chr, range(256)) if a.isalnum()]:
|
||||
st[c] = SYNTAX_WORD
|
||||
st["\n"] = st[" "] = SYNTAX_WHITESPACE
|
||||
return st
|
||||
|
||||
|
||||
def make_default_commands() -> dict[CommandName, type[Command]]:
|
||||
result: dict[CommandName, type[Command]] = {}
|
||||
for v in vars(commands).values():
|
||||
if isinstance(v, type) and issubclass(v, Command) and v.__name__[0].islower():
|
||||
result[v.__name__] = v
|
||||
result[v.__name__.replace("_", "-")] = v
|
||||
return result
|
||||
|
||||
|
||||
default_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple(
|
||||
[
|
||||
(r"\C-a", "beginning-of-line"),
|
||||
(r"\C-b", "left"),
|
||||
(r"\C-c", "interrupt"),
|
||||
(r"\C-d", "delete"),
|
||||
(r"\C-e", "end-of-line"),
|
||||
(r"\C-f", "right"),
|
||||
(r"\C-g", "cancel"),
|
||||
(r"\C-h", "backspace"),
|
||||
(r"\C-j", "accept"),
|
||||
(r"\<return>", "accept"),
|
||||
(r"\C-k", "kill-line"),
|
||||
(r"\C-l", "clear-screen"),
|
||||
(r"\C-m", "accept"),
|
||||
(r"\C-t", "transpose-characters"),
|
||||
(r"\C-u", "unix-line-discard"),
|
||||
(r"\C-w", "unix-word-rubout"),
|
||||
(r"\C-x\C-u", "upcase-region"),
|
||||
(r"\C-y", "yank"),
|
||||
*(() if sys.platform == "win32" else ((r"\C-z", "suspend"), )),
|
||||
(r"\M-b", "backward-word"),
|
||||
(r"\M-c", "capitalize-word"),
|
||||
(r"\M-d", "kill-word"),
|
||||
(r"\M-f", "forward-word"),
|
||||
(r"\M-l", "downcase-word"),
|
||||
(r"\M-t", "transpose-words"),
|
||||
(r"\M-u", "upcase-word"),
|
||||
(r"\M-y", "yank-pop"),
|
||||
(r"\M--", "digit-arg"),
|
||||
(r"\M-0", "digit-arg"),
|
||||
(r"\M-1", "digit-arg"),
|
||||
(r"\M-2", "digit-arg"),
|
||||
(r"\M-3", "digit-arg"),
|
||||
(r"\M-4", "digit-arg"),
|
||||
(r"\M-5", "digit-arg"),
|
||||
(r"\M-6", "digit-arg"),
|
||||
(r"\M-7", "digit-arg"),
|
||||
(r"\M-8", "digit-arg"),
|
||||
(r"\M-9", "digit-arg"),
|
||||
(r"\M-\n", "accept"),
|
||||
("\\\\", "self-insert"),
|
||||
(r"\x1b[200~", "enable_bracketed_paste"),
|
||||
(r"\x1b[201~", "disable_bracketed_paste"),
|
||||
(r"\x03", "ctrl-c"),
|
||||
]
|
||||
+ [(c, "self-insert") for c in map(chr, range(32, 127)) if c != "\\"]
|
||||
+ [(c, "self-insert") for c in map(chr, range(128, 256)) if c.isalpha()]
|
||||
+ [
|
||||
(r"\<up>", "up"),
|
||||
(r"\<down>", "down"),
|
||||
(r"\<left>", "left"),
|
||||
(r"\C-\<left>", "backward-word"),
|
||||
(r"\<right>", "right"),
|
||||
(r"\C-\<right>", "forward-word"),
|
||||
(r"\<delete>", "delete"),
|
||||
(r"\x1b[3~", "delete"),
|
||||
(r"\<backspace>", "backspace"),
|
||||
(r"\M-\<backspace>", "backward-kill-word"),
|
||||
(r"\<end>", "end-of-line"), # was 'end'
|
||||
(r"\<home>", "beginning-of-line"), # was 'home'
|
||||
(r"\<f1>", "help"),
|
||||
(r"\<f2>", "show-history"),
|
||||
(r"\<f3>", "paste-mode"),
|
||||
(r"\EOF", "end"), # the entries in the terminfo database for xterms
|
||||
(r"\EOH", "home"), # seem to be wrong. this is a less than ideal
|
||||
# workaround
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Reader:
|
||||
"""The Reader class implements the bare bones of a command reader,
|
||||
handling such details as editing and cursor motion. What it does
|
||||
not support are such things as completion or history support -
|
||||
these are implemented elsewhere.
|
||||
|
||||
Instance variables of note include:
|
||||
|
||||
* buffer:
|
||||
A *list* (*not* a string at the moment :-) containing all the
|
||||
characters that have been entered.
|
||||
* console:
|
||||
Hopefully encapsulates the OS dependent stuff.
|
||||
* pos:
|
||||
A 0-based index into `buffer' for where the insertion point
|
||||
is.
|
||||
* screeninfo:
|
||||
Ahem. This list contains some info needed to move the
|
||||
insertion point around reasonably efficiently.
|
||||
* cxy, lxy:
|
||||
the position of the insertion point in screen ...
|
||||
* syntax_table:
|
||||
Dictionary mapping characters to `syntax class'; read the
|
||||
emacs docs to see what this means :-)
|
||||
* commands:
|
||||
Dictionary mapping command names to command classes.
|
||||
* arg:
|
||||
The emacs-style prefix argument. It will be None if no such
|
||||
argument has been provided.
|
||||
* dirty:
|
||||
True if we need to refresh the display.
|
||||
* kill_ring:
|
||||
The emacs-style kill-ring; manipulated with yank & yank-pop
|
||||
* ps1, ps2, ps3, ps4:
|
||||
prompts. ps1 is the prompt for a one-line input; for a
|
||||
multiline input it looks like:
|
||||
ps2> first line of input goes here
|
||||
ps3> second and further
|
||||
ps3> lines get ps3
|
||||
...
|
||||
ps4> and the last one gets ps4
|
||||
As with the usual top-level, you can set these to instances if
|
||||
you like; str() will be called on them (once) at the beginning
|
||||
of each command. Don't put really long or newline containing
|
||||
strings here, please!
|
||||
This is just the default policy; you can change it freely by
|
||||
overriding get_prompt() (and indeed some standard subclasses
|
||||
do).
|
||||
* finished:
|
||||
handle1 will set this to a true value if a command signals
|
||||
that we're done.
|
||||
"""
|
||||
|
||||
console: console.Console
|
||||
|
||||
## state
|
||||
buffer: list[str] = field(default_factory=list)
|
||||
pos: int = 0
|
||||
ps1: str = "->> "
|
||||
ps2: str = "/>> "
|
||||
ps3: str = "|.. "
|
||||
ps4: str = R"\__ "
|
||||
kill_ring: list[list[str]] = field(default_factory=list)
|
||||
msg: str = ""
|
||||
arg: int | None = None
|
||||
dirty: bool = False
|
||||
finished: bool = False
|
||||
paste_mode: bool = False
|
||||
in_bracketed_paste: bool = False
|
||||
commands: dict[str, type[Command]] = field(default_factory=make_default_commands)
|
||||
last_command: type[Command] | None = None
|
||||
syntax_table: dict[str, int] = field(default_factory=make_default_syntax_table)
|
||||
keymap: tuple[tuple[str, str], ...] = ()
|
||||
input_trans: input.KeymapTranslator = field(init=False)
|
||||
input_trans_stack: list[input.KeymapTranslator] = field(default_factory=list)
|
||||
screen: list[str] = field(default_factory=list)
|
||||
screeninfo: list[tuple[int, list[int]]] = field(init=False)
|
||||
cxy: tuple[int, int] = field(init=False)
|
||||
lxy: tuple[int, int] = field(init=False)
|
||||
scheduled_commands: list[str] = field(default_factory=list)
|
||||
can_colorize: bool = False
|
||||
threading_hook: Callback | None = None
|
||||
|
||||
## cached metadata to speed up screen refreshes
|
||||
@dataclass
|
||||
class RefreshCache:
|
||||
in_bracketed_paste: bool = False
|
||||
screen: list[str] = field(default_factory=list)
|
||||
screeninfo: list[tuple[int, list[int]]] = field(init=False)
|
||||
line_end_offsets: list[int] = field(default_factory=list)
|
||||
pos: int = field(init=False)
|
||||
cxy: tuple[int, int] = field(init=False)
|
||||
dimensions: tuple[int, int] = field(init=False)
|
||||
invalidated: bool = False
|
||||
|
||||
def update_cache(self,
|
||||
reader: Reader,
|
||||
screen: list[str],
|
||||
screeninfo: list[tuple[int, list[int]]],
|
||||
) -> None:
|
||||
self.in_bracketed_paste = reader.in_bracketed_paste
|
||||
self.screen = screen.copy()
|
||||
self.screeninfo = screeninfo.copy()
|
||||
self.pos = reader.pos
|
||||
self.cxy = reader.cxy
|
||||
self.dimensions = reader.console.width, reader.console.height
|
||||
self.invalidated = False
|
||||
|
||||
def valid(self, reader: Reader) -> bool:
|
||||
if self.invalidated:
|
||||
return False
|
||||
dimensions = reader.console.width, reader.console.height
|
||||
dimensions_changed = dimensions != self.dimensions
|
||||
paste_changed = reader.in_bracketed_paste != self.in_bracketed_paste
|
||||
return not (dimensions_changed or paste_changed)
|
||||
|
||||
def get_cached_location(self, reader: Reader) -> tuple[int, int]:
|
||||
if self.invalidated:
|
||||
raise ValueError("Cache is invalidated")
|
||||
offset = 0
|
||||
earliest_common_pos = min(reader.pos, self.pos)
|
||||
num_common_lines = len(self.line_end_offsets)
|
||||
while num_common_lines > 0:
|
||||
offset = self.line_end_offsets[num_common_lines - 1]
|
||||
if earliest_common_pos > offset:
|
||||
break
|
||||
num_common_lines -= 1
|
||||
else:
|
||||
offset = 0
|
||||
return offset, num_common_lines
|
||||
|
||||
last_refresh_cache: RefreshCache = field(default_factory=RefreshCache)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Enable the use of `insert` without a `prepare` call - necessary to
|
||||
# facilitate the tab completion hack implemented for
|
||||
# <https://bugs.python.org/issue25660>.
|
||||
self.keymap = self.collect_keymap()
|
||||
self.input_trans = input.KeymapTranslator(
|
||||
self.keymap, invalid_cls="invalid-key", character_cls="self-insert"
|
||||
)
|
||||
self.screeninfo = [(0, [])]
|
||||
self.cxy = self.pos2xy()
|
||||
self.lxy = (self.pos, 0)
|
||||
self.can_colorize = can_colorize()
|
||||
|
||||
self.last_refresh_cache.screeninfo = self.screeninfo
|
||||
self.last_refresh_cache.pos = self.pos
|
||||
self.last_refresh_cache.cxy = self.cxy
|
||||
self.last_refresh_cache.dimensions = (0, 0)
|
||||
|
||||
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
|
||||
return default_keymap
|
||||
|
||||
def calc_screen(self) -> list[str]:
|
||||
"""Translate changes in self.buffer into changes in self.console.screen."""
|
||||
# Since the last call to calc_screen:
|
||||
# screen and screeninfo may differ due to a completion menu being shown
|
||||
# pos and cxy may differ due to edits, cursor movements, or completion menus
|
||||
|
||||
# Lines that are above both the old and new cursor position can't have changed,
|
||||
# unless the terminal has been resized (which might cause reflowing) or we've
|
||||
# entered or left paste mode (which changes prompts, causing reflowing).
|
||||
num_common_lines = 0
|
||||
offset = 0
|
||||
if self.last_refresh_cache.valid(self):
|
||||
offset, num_common_lines = self.last_refresh_cache.get_cached_location(self)
|
||||
|
||||
screen = self.last_refresh_cache.screen
|
||||
del screen[num_common_lines:]
|
||||
|
||||
screeninfo = self.last_refresh_cache.screeninfo
|
||||
del screeninfo[num_common_lines:]
|
||||
|
||||
last_refresh_line_end_offsets = self.last_refresh_cache.line_end_offsets
|
||||
del last_refresh_line_end_offsets[num_common_lines:]
|
||||
|
||||
pos = self.pos
|
||||
pos -= offset
|
||||
|
||||
prompt_from_cache = (offset and self.buffer[offset - 1] != "\n")
|
||||
|
||||
lines = "".join(self.buffer[offset:]).split("\n")
|
||||
|
||||
cursor_found = False
|
||||
lines_beyond_cursor = 0
|
||||
for ln, line in enumerate(lines, num_common_lines):
|
||||
ll = len(line)
|
||||
if 0 <= pos <= ll:
|
||||
self.lxy = pos, ln
|
||||
cursor_found = True
|
||||
elif cursor_found:
|
||||
lines_beyond_cursor += 1
|
||||
if lines_beyond_cursor > self.console.height:
|
||||
# No need to keep formatting lines.
|
||||
# The console can't show them.
|
||||
break
|
||||
if prompt_from_cache:
|
||||
# Only the first line's prompt can come from the cache
|
||||
prompt_from_cache = False
|
||||
prompt = ""
|
||||
else:
|
||||
prompt = self.get_prompt(ln, ll >= pos >= 0)
|
||||
while "\n" in prompt:
|
||||
pre_prompt, _, prompt = prompt.partition("\n")
|
||||
last_refresh_line_end_offsets.append(offset)
|
||||
screen.append(pre_prompt)
|
||||
screeninfo.append((0, []))
|
||||
pos -= ll + 1
|
||||
prompt, lp = self.process_prompt(prompt)
|
||||
l, l2 = disp_str(line)
|
||||
wrapcount = (wlen(l) + lp) // self.console.width
|
||||
if wrapcount == 0:
|
||||
offset += ll + 1 # Takes all of the line plus the newline
|
||||
last_refresh_line_end_offsets.append(offset)
|
||||
screen.append(prompt + l)
|
||||
screeninfo.append((lp, l2))
|
||||
else:
|
||||
i = 0
|
||||
while l:
|
||||
prelen = lp if i == 0 else 0
|
||||
index_to_wrap_before = 0
|
||||
column = 0
|
||||
for character_width in l2:
|
||||
if column + character_width >= self.console.width - prelen:
|
||||
break
|
||||
index_to_wrap_before += 1
|
||||
column += character_width
|
||||
pre = prompt if i == 0 else ""
|
||||
if len(l) > index_to_wrap_before:
|
||||
offset += index_to_wrap_before
|
||||
post = "\\"
|
||||
after = [1]
|
||||
else:
|
||||
offset += index_to_wrap_before + 1 # Takes the newline
|
||||
post = ""
|
||||
after = []
|
||||
last_refresh_line_end_offsets.append(offset)
|
||||
screen.append(pre + l[:index_to_wrap_before] + post)
|
||||
screeninfo.append((prelen, l2[:index_to_wrap_before] + after))
|
||||
l = l[index_to_wrap_before:]
|
||||
l2 = l2[index_to_wrap_before:]
|
||||
i += 1
|
||||
self.screeninfo = screeninfo
|
||||
self.cxy = self.pos2xy()
|
||||
if self.msg:
|
||||
for mline in self.msg.split("\n"):
|
||||
screen.append(mline)
|
||||
screeninfo.append((0, []))
|
||||
|
||||
self.last_refresh_cache.update_cache(self, screen, screeninfo)
|
||||
return screen
|
||||
|
||||
@staticmethod
|
||||
def process_prompt(prompt: str) -> tuple[str, int]:
|
||||
"""Process the prompt.
|
||||
|
||||
This means calculate the length of the prompt. The character \x01
|
||||
and \x02 are used to bracket ANSI control sequences and need to be
|
||||
excluded from the length calculation. So also a copy of the prompt
|
||||
is returned with these control characters removed."""
|
||||
|
||||
# The logic below also ignores the length of common escape
|
||||
# sequences if they were not explicitly within \x01...\x02.
|
||||
# They are CSI (or ANSI) sequences ( ESC [ ... LETTER )
|
||||
|
||||
# wlen from utils already excludes ANSI_ESCAPE_SEQUENCE chars,
|
||||
# which breaks the logic below so we redefine it here.
|
||||
def wlen(s: str) -> int:
|
||||
return sum(str_width(i) for i in s)
|
||||
|
||||
out_prompt = ""
|
||||
l = wlen(prompt)
|
||||
pos = 0
|
||||
while True:
|
||||
s = prompt.find("\x01", pos)
|
||||
if s == -1:
|
||||
break
|
||||
e = prompt.find("\x02", s)
|
||||
if e == -1:
|
||||
break
|
||||
# Found start and end brackets, subtract from string length
|
||||
l = l - (e - s + 1)
|
||||
keep = prompt[pos:s]
|
||||
l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep)))
|
||||
out_prompt += keep + prompt[s + 1 : e]
|
||||
pos = e + 1
|
||||
keep = prompt[pos:]
|
||||
l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep)))
|
||||
out_prompt += keep
|
||||
return out_prompt, l
|
||||
|
||||
def bow(self, p: int | None = None) -> int:
|
||||
"""Return the 0-based index of the word break preceding p most
|
||||
immediately.
|
||||
|
||||
p defaults to self.pos; word boundaries are determined using
|
||||
self.syntax_table."""
|
||||
if p is None:
|
||||
p = self.pos
|
||||
st = self.syntax_table
|
||||
b = self.buffer
|
||||
p -= 1
|
||||
while p >= 0 and st.get(b[p], SYNTAX_WORD) != SYNTAX_WORD:
|
||||
p -= 1
|
||||
while p >= 0 and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
|
||||
p -= 1
|
||||
return p + 1
|
||||
|
||||
def eow(self, p: int | None = None) -> int:
|
||||
"""Return the 0-based index of the word break following p most
|
||||
immediately.
|
||||
|
||||
p defaults to self.pos; word boundaries are determined using
|
||||
self.syntax_table."""
|
||||
if p is None:
|
||||
p = self.pos
|
||||
st = self.syntax_table
|
||||
b = self.buffer
|
||||
while p < len(b) and st.get(b[p], SYNTAX_WORD) != SYNTAX_WORD:
|
||||
p += 1
|
||||
while p < len(b) and st.get(b[p], SYNTAX_WORD) == SYNTAX_WORD:
|
||||
p += 1
|
||||
return p
|
||||
|
||||
def bol(self, p: int | None = None) -> int:
|
||||
"""Return the 0-based index of the line break preceding p most
|
||||
immediately.
|
||||
|
||||
p defaults to self.pos."""
|
||||
if p is None:
|
||||
p = self.pos
|
||||
b = self.buffer
|
||||
p -= 1
|
||||
while p >= 0 and b[p] != "\n":
|
||||
p -= 1
|
||||
return p + 1
|
||||
|
||||
def eol(self, p: int | None = None) -> int:
|
||||
"""Return the 0-based index of the line break following p most
|
||||
immediately.
|
||||
|
||||
p defaults to self.pos."""
|
||||
if p is None:
|
||||
p = self.pos
|
||||
b = self.buffer
|
||||
while p < len(b) and b[p] != "\n":
|
||||
p += 1
|
||||
return p
|
||||
|
||||
def max_column(self, y: int) -> int:
|
||||
"""Return the last x-offset for line y"""
|
||||
return self.screeninfo[y][0] + sum(self.screeninfo[y][1])
|
||||
|
||||
def max_row(self) -> int:
|
||||
return len(self.screeninfo) - 1
|
||||
|
||||
def get_arg(self, default: int = 1) -> int:
|
||||
"""Return any prefix argument that the user has supplied,
|
||||
returning `default' if there is None. Defaults to 1.
|
||||
"""
|
||||
if self.arg is None:
|
||||
return default
|
||||
return self.arg
|
||||
|
||||
def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
|
||||
"""Return what should be in the left-hand margin for line
|
||||
`lineno'."""
|
||||
if self.arg is not None and cursor_on_line:
|
||||
prompt = f"(arg: {self.arg}) "
|
||||
elif self.paste_mode and not self.in_bracketed_paste:
|
||||
prompt = "(paste) "
|
||||
elif "\n" in self.buffer:
|
||||
if lineno == 0:
|
||||
prompt = self.ps2
|
||||
elif self.ps4 and lineno == self.buffer.count("\n"):
|
||||
prompt = self.ps4
|
||||
else:
|
||||
prompt = self.ps3
|
||||
else:
|
||||
prompt = self.ps1
|
||||
|
||||
if self.can_colorize:
|
||||
prompt = f"{ANSIColors.BOLD_MAGENTA}{prompt}{ANSIColors.RESET}"
|
||||
return prompt
|
||||
|
||||
def push_input_trans(self, itrans: input.KeymapTranslator) -> None:
|
||||
self.input_trans_stack.append(self.input_trans)
|
||||
self.input_trans = itrans
|
||||
|
||||
def pop_input_trans(self) -> None:
|
||||
self.input_trans = self.input_trans_stack.pop()
|
||||
|
||||
def setpos_from_xy(self, x: int, y: int) -> None:
|
||||
"""Set pos according to coordinates x, y"""
|
||||
pos = 0
|
||||
i = 0
|
||||
while i < y:
|
||||
prompt_len, character_widths = self.screeninfo[i]
|
||||
offset = len(character_widths) - character_widths.count(0)
|
||||
in_wrapped_line = prompt_len + sum(character_widths) >= self.console.width
|
||||
if in_wrapped_line:
|
||||
pos += offset - 1 # -1 cause backslash is not in buffer
|
||||
else:
|
||||
pos += offset + 1 # +1 cause newline is in buffer
|
||||
i += 1
|
||||
|
||||
j = 0
|
||||
cur_x = self.screeninfo[i][0]
|
||||
while cur_x < x:
|
||||
if self.screeninfo[i][1][j] == 0:
|
||||
continue
|
||||
cur_x += self.screeninfo[i][1][j]
|
||||
j += 1
|
||||
pos += 1
|
||||
|
||||
self.pos = pos
|
||||
|
||||
def pos2xy(self) -> tuple[int, int]:
|
||||
"""Return the x, y coordinates of position 'pos'."""
|
||||
# this *is* incomprehensible, yes.
|
||||
p, y = 0, 0
|
||||
l2: list[int] = []
|
||||
pos = self.pos
|
||||
assert 0 <= pos <= len(self.buffer)
|
||||
if pos == len(self.buffer) and len(self.screeninfo) > 0:
|
||||
y = len(self.screeninfo) - 1
|
||||
p, l2 = self.screeninfo[y]
|
||||
return p + sum(l2) + l2.count(0), y
|
||||
|
||||
for p, l2 in self.screeninfo:
|
||||
l = len(l2) - l2.count(0)
|
||||
in_wrapped_line = p + sum(l2) >= self.console.width
|
||||
offset = l - 1 if in_wrapped_line else l # need to remove backslash
|
||||
if offset >= pos:
|
||||
break
|
||||
|
||||
if p + sum(l2) >= self.console.width:
|
||||
pos -= l - 1 # -1 cause backslash is not in buffer
|
||||
else:
|
||||
pos -= l + 1 # +1 cause newline is in buffer
|
||||
y += 1
|
||||
return p + sum(l2[:pos]), y
|
||||
|
||||
def insert(self, text: str | list[str]) -> None:
|
||||
"""Insert 'text' at the insertion point."""
|
||||
self.buffer[self.pos : self.pos] = list(text)
|
||||
self.pos += len(text)
|
||||
self.dirty = True
|
||||
|
||||
def update_cursor(self) -> None:
|
||||
"""Move the cursor to reflect changes in self.pos"""
|
||||
self.cxy = self.pos2xy()
|
||||
self.console.move_cursor(*self.cxy)
|
||||
|
||||
def after_command(self, cmd: Command) -> None:
|
||||
"""This function is called to allow post command cleanup."""
|
||||
if getattr(cmd, "kills_digit_arg", True):
|
||||
if self.arg is not None:
|
||||
self.dirty = True
|
||||
self.arg = None
|
||||
|
||||
def prepare(self) -> None:
|
||||
"""Get ready to run. Call restore when finished. You must not
|
||||
write to the console in between the calls to prepare and
|
||||
restore."""
|
||||
try:
|
||||
self.console.prepare()
|
||||
self.arg = None
|
||||
self.finished = False
|
||||
del self.buffer[:]
|
||||
self.pos = 0
|
||||
self.dirty = True
|
||||
self.last_command = None
|
||||
self.calc_screen()
|
||||
except BaseException:
|
||||
self.restore()
|
||||
raise
|
||||
|
||||
while self.scheduled_commands:
|
||||
cmd = self.scheduled_commands.pop()
|
||||
self.do_cmd((cmd, []))
|
||||
|
||||
def last_command_is(self, cls: type) -> bool:
|
||||
if not self.last_command:
|
||||
return False
|
||||
return issubclass(cls, self.last_command)
|
||||
|
||||
def restore(self) -> None:
|
||||
"""Clean up after a run."""
|
||||
self.console.restore()
|
||||
|
||||
@contextmanager
|
||||
def suspend(self) -> SimpleContextManager:
|
||||
"""A context manager to delegate to another reader."""
|
||||
prev_state = {f.name: getattr(self, f.name) for f in fields(self)}
|
||||
try:
|
||||
self.restore()
|
||||
yield
|
||||
finally:
|
||||
for arg in ("msg", "ps1", "ps2", "ps3", "ps4", "paste_mode"):
|
||||
setattr(self, arg, prev_state[arg])
|
||||
self.prepare()
|
||||
|
||||
def finish(self) -> None:
|
||||
"""Called when a command signals that we're finished."""
|
||||
pass
|
||||
|
||||
def error(self, msg: str = "none") -> None:
|
||||
self.msg = "! " + msg + " "
|
||||
self.dirty = True
|
||||
self.console.beep()
|
||||
|
||||
def update_screen(self) -> None:
|
||||
if self.dirty:
|
||||
self.refresh()
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Recalculate and refresh the screen."""
|
||||
if self.in_bracketed_paste and self.buffer and not self.buffer[-1] == "\n":
|
||||
return
|
||||
|
||||
# this call sets up self.cxy, so call it first.
|
||||
self.screen = self.calc_screen()
|
||||
self.console.refresh(self.screen, self.cxy)
|
||||
self.dirty = False
|
||||
|
||||
def do_cmd(self, cmd: tuple[str, list[str]]) -> None:
|
||||
"""`cmd` is a tuple of "event_name" and "event", which in the current
|
||||
implementation is always just the "buffer" which happens to be a list
|
||||
of single-character strings."""
|
||||
|
||||
trace("received command {cmd}", cmd=cmd)
|
||||
if isinstance(cmd[0], str):
|
||||
command_type = self.commands.get(cmd[0], commands.invalid_command)
|
||||
elif isinstance(cmd[0], type):
|
||||
command_type = cmd[0]
|
||||
else:
|
||||
return # nothing to do
|
||||
|
||||
command = command_type(self, *cmd) # type: ignore[arg-type]
|
||||
command.do()
|
||||
|
||||
self.after_command(command)
|
||||
|
||||
if self.dirty:
|
||||
self.refresh()
|
||||
else:
|
||||
self.update_cursor()
|
||||
|
||||
if not isinstance(cmd, commands.digit_arg):
|
||||
self.last_command = command_type
|
||||
|
||||
self.finished = bool(command.finish)
|
||||
if self.finished:
|
||||
self.console.finish()
|
||||
self.finish()
|
||||
|
||||
def run_hooks(self) -> None:
|
||||
threading_hook = self.threading_hook
|
||||
if threading_hook is None and 'threading' in sys.modules:
|
||||
from ._threading_handler import install_threading_hook
|
||||
install_threading_hook(self)
|
||||
if threading_hook is not None:
|
||||
try:
|
||||
threading_hook()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
input_hook = self.console.input_hook
|
||||
if input_hook:
|
||||
try:
|
||||
input_hook()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def handle1(self, block: bool = True) -> bool:
|
||||
"""Handle a single event. Wait as long as it takes if block
|
||||
is true (the default), otherwise return False if no event is
|
||||
pending."""
|
||||
|
||||
if self.msg:
|
||||
self.msg = ""
|
||||
self.dirty = True
|
||||
|
||||
while True:
|
||||
# We use the same timeout as in readline.c: 100ms
|
||||
self.run_hooks()
|
||||
self.console.wait(100)
|
||||
event = self.console.get_event(block=False)
|
||||
if not event:
|
||||
if block:
|
||||
continue
|
||||
return False
|
||||
|
||||
translate = True
|
||||
|
||||
if event.evt == "key":
|
||||
self.input_trans.push(event)
|
||||
elif event.evt == "scroll":
|
||||
self.refresh()
|
||||
elif event.evt == "resize":
|
||||
self.refresh()
|
||||
else:
|
||||
translate = False
|
||||
|
||||
if translate:
|
||||
cmd = self.input_trans.get()
|
||||
else:
|
||||
cmd = [event.evt, event.data]
|
||||
|
||||
if cmd is None:
|
||||
if block:
|
||||
continue
|
||||
return False
|
||||
|
||||
self.do_cmd(cmd)
|
||||
return True
|
||||
|
||||
def push_char(self, char: int | bytes) -> None:
|
||||
self.console.push_char(char)
|
||||
self.handle1(block=False)
|
||||
|
||||
def readline(self, startup_hook: Callback | None = None) -> str:
|
||||
"""Read a line. The implementation of this method also shows
|
||||
how to drive Reader if you want more control over the event
|
||||
loop."""
|
||||
self.prepare()
|
||||
try:
|
||||
if startup_hook is not None:
|
||||
startup_hook()
|
||||
self.refresh()
|
||||
while not self.finished:
|
||||
self.handle1()
|
||||
return self.get_unicode()
|
||||
|
||||
finally:
|
||||
self.restore()
|
||||
|
||||
def bind(self, spec: KeySpec, command: CommandName) -> None:
|
||||
self.keymap = self.keymap + ((spec, command),)
|
||||
self.input_trans = input.KeymapTranslator(
|
||||
self.keymap, invalid_cls="invalid-key", character_cls="self-insert"
|
||||
)
|
||||
|
||||
def get_unicode(self) -> str:
|
||||
"""Return the current buffer as a unicode string."""
|
||||
return "".join(self.buffer)
|
||||
598
Lib/_pyrepl/readline.py
vendored
Normal file
598
Lib/_pyrepl/readline.py
vendored
Normal file
@@ -0,0 +1,598 @@
|
||||
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Alex Gaynor
|
||||
# Antonio Cuni
|
||||
# Armin Rigo
|
||||
# Holger Krekel
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
"""A compatibility wrapper reimplementing the 'readline' standard module
|
||||
on top of pyrepl. Not all functionalities are supported. Contains
|
||||
extensions for multiline input.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import os
|
||||
from site import gethistoryfile # type: ignore[attr-defined]
|
||||
import sys
|
||||
from rlcompleter import Completer as RLCompleter
|
||||
|
||||
from . import commands, historical_reader
|
||||
from .completing_reader import CompletingReader
|
||||
from .console import Console as ConsoleType
|
||||
|
||||
Console: type[ConsoleType]
|
||||
_error: tuple[type[Exception], ...] | type[Exception]
|
||||
try:
|
||||
from .unix_console import UnixConsole as Console, _error
|
||||
except ImportError:
|
||||
from .windows_console import WindowsConsole as Console, _error
|
||||
|
||||
ENCODING = sys.getdefaultencoding() or "latin1"
|
||||
|
||||
|
||||
# types
|
||||
Command = commands.Command
|
||||
from collections.abc import Callable, Collection
|
||||
from .types import Callback, Completer, KeySpec, CommandName
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
MoreLinesCallable = Callable[[str], bool]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"add_history",
|
||||
"clear_history",
|
||||
"get_begidx",
|
||||
"get_completer",
|
||||
"get_completer_delims",
|
||||
"get_current_history_length",
|
||||
"get_endidx",
|
||||
"get_history_item",
|
||||
"get_history_length",
|
||||
"get_line_buffer",
|
||||
"insert_text",
|
||||
"parse_and_bind",
|
||||
"read_history_file",
|
||||
# "read_init_file",
|
||||
# "redisplay",
|
||||
"remove_history_item",
|
||||
"replace_history_item",
|
||||
"set_auto_history",
|
||||
"set_completer",
|
||||
"set_completer_delims",
|
||||
"set_history_length",
|
||||
# "set_pre_input_hook",
|
||||
"set_startup_hook",
|
||||
"write_history_file",
|
||||
# ---- multiline extensions ----
|
||||
"multiline_input",
|
||||
]
|
||||
|
||||
# ____________________________________________________________
|
||||
|
||||
@dataclass
|
||||
class ReadlineConfig:
|
||||
readline_completer: Completer | None = None
|
||||
completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?")
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
|
||||
# Class fields
|
||||
assume_immutable_completions = False
|
||||
use_brackets = False
|
||||
sort_in_column = True
|
||||
|
||||
# Instance fields
|
||||
config: ReadlineConfig
|
||||
more_lines: MoreLinesCallable | None = None
|
||||
last_used_indentation: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
super().__post_init__()
|
||||
self.commands["maybe_accept"] = maybe_accept
|
||||
self.commands["maybe-accept"] = maybe_accept
|
||||
self.commands["backspace_dedent"] = backspace_dedent
|
||||
self.commands["backspace-dedent"] = backspace_dedent
|
||||
|
||||
def error(self, msg: str = "none") -> None:
|
||||
pass # don't show error messages by default
|
||||
|
||||
def get_stem(self) -> str:
|
||||
b = self.buffer
|
||||
p = self.pos - 1
|
||||
completer_delims = self.config.completer_delims
|
||||
while p >= 0 and b[p] not in completer_delims:
|
||||
p -= 1
|
||||
return "".join(b[p + 1 : self.pos])
|
||||
|
||||
def get_completions(self, stem: str) -> list[str]:
|
||||
if len(stem) == 0 and self.more_lines is not None:
|
||||
b = self.buffer
|
||||
p = self.pos
|
||||
while p > 0 and b[p - 1] != "\n":
|
||||
p -= 1
|
||||
num_spaces = 4 - ((self.pos - p) % 4)
|
||||
return [" " * num_spaces]
|
||||
result = []
|
||||
function = self.config.readline_completer
|
||||
if function is not None:
|
||||
try:
|
||||
stem = str(stem) # rlcompleter.py seems to not like unicode
|
||||
except UnicodeEncodeError:
|
||||
pass # but feed unicode anyway if we have no choice
|
||||
state = 0
|
||||
while True:
|
||||
try:
|
||||
next = function(stem, state)
|
||||
except Exception:
|
||||
break
|
||||
if not isinstance(next, str):
|
||||
break
|
||||
result.append(next)
|
||||
state += 1
|
||||
# emulate the behavior of the standard readline that sorts
|
||||
# the completions before displaying them.
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
def get_trimmed_history(self, maxlength: int) -> list[str]:
|
||||
if maxlength >= 0:
|
||||
cut = len(self.history) - maxlength
|
||||
if cut < 0:
|
||||
cut = 0
|
||||
else:
|
||||
cut = 0
|
||||
return self.history[cut:]
|
||||
|
||||
def update_last_used_indentation(self) -> None:
|
||||
indentation = _get_first_indentation(self.buffer)
|
||||
if indentation is not None:
|
||||
self.last_used_indentation = indentation
|
||||
|
||||
# --- simplified support for reading multiline Python statements ---
|
||||
|
||||
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
|
||||
return super().collect_keymap() + (
|
||||
(r"\n", "maybe-accept"),
|
||||
(r"\<backspace>", "backspace-dedent"),
|
||||
)
|
||||
|
||||
def after_command(self, cmd: Command) -> None:
|
||||
super().after_command(cmd)
|
||||
if self.more_lines is None:
|
||||
# Force single-line input if we are in raw_input() mode.
|
||||
# Although there is no direct way to add a \n in this mode,
|
||||
# multiline buffers can still show up using various
|
||||
# commands, e.g. navigating the history.
|
||||
try:
|
||||
index = self.buffer.index("\n")
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
self.buffer = self.buffer[:index]
|
||||
if self.pos > len(self.buffer):
|
||||
self.pos = len(self.buffer)
|
||||
|
||||
|
||||
def set_auto_history(_should_auto_add_history: bool) -> None:
|
||||
"""Enable or disable automatic history"""
|
||||
historical_reader.should_auto_add_history = bool(_should_auto_add_history)
|
||||
|
||||
|
||||
def _get_this_line_indent(buffer: list[str], pos: int) -> int:
|
||||
indent = 0
|
||||
while pos > 0 and buffer[pos - 1] in " \t":
|
||||
indent += 1
|
||||
pos -= 1
|
||||
if pos > 0 and buffer[pos - 1] == "\n":
|
||||
return indent
|
||||
return 0
|
||||
|
||||
|
||||
def _get_previous_line_indent(buffer: list[str], pos: int) -> tuple[int, int | None]:
|
||||
prevlinestart = pos
|
||||
while prevlinestart > 0 and buffer[prevlinestart - 1] != "\n":
|
||||
prevlinestart -= 1
|
||||
prevlinetext = prevlinestart
|
||||
while prevlinetext < pos and buffer[prevlinetext] in " \t":
|
||||
prevlinetext += 1
|
||||
if prevlinetext == pos:
|
||||
indent = None
|
||||
else:
|
||||
indent = prevlinetext - prevlinestart
|
||||
return prevlinestart, indent
|
||||
|
||||
|
||||
def _get_first_indentation(buffer: list[str]) -> str | None:
|
||||
indented_line_start = None
|
||||
for i in range(len(buffer)):
|
||||
if (i < len(buffer) - 1
|
||||
and buffer[i] == "\n"
|
||||
and buffer[i + 1] in " \t"
|
||||
):
|
||||
indented_line_start = i + 1
|
||||
elif indented_line_start is not None and buffer[i] not in " \t\n":
|
||||
return ''.join(buffer[indented_line_start : i])
|
||||
return None
|
||||
|
||||
|
||||
def _should_auto_indent(buffer: list[str], pos: int) -> bool:
|
||||
# check if last character before "pos" is a colon, ignoring
|
||||
# whitespaces and comments.
|
||||
last_char = None
|
||||
while pos > 0:
|
||||
pos -= 1
|
||||
if last_char is None:
|
||||
if buffer[pos] not in " \t\n#": # ignore whitespaces and comments
|
||||
last_char = buffer[pos]
|
||||
else:
|
||||
# even if we found a non-whitespace character before
|
||||
# original pos, we keep going back until newline is reached
|
||||
# to make sure we ignore comments
|
||||
if buffer[pos] == "\n":
|
||||
break
|
||||
if buffer[pos] == "#":
|
||||
last_char = None
|
||||
return last_char == ":"
|
||||
|
||||
|
||||
class maybe_accept(commands.Command):
|
||||
def do(self) -> None:
|
||||
r: ReadlineAlikeReader
|
||||
r = self.reader # type: ignore[assignment]
|
||||
r.dirty = True # this is needed to hide the completion menu, if visible
|
||||
|
||||
if self.reader.in_bracketed_paste:
|
||||
r.insert("\n")
|
||||
return
|
||||
|
||||
# if there are already several lines and the cursor
|
||||
# is not on the last one, always insert a new \n.
|
||||
text = r.get_unicode()
|
||||
|
||||
if "\n" in r.buffer[r.pos :] or (
|
||||
r.more_lines is not None and r.more_lines(text)
|
||||
):
|
||||
def _newline_before_pos():
|
||||
before_idx = r.pos - 1
|
||||
while before_idx > 0 and text[before_idx].isspace():
|
||||
before_idx -= 1
|
||||
return text[before_idx : r.pos].count("\n") > 0
|
||||
|
||||
# if there's already a new line before the cursor then
|
||||
# even if the cursor is followed by whitespace, we assume
|
||||
# the user is trying to terminate the block
|
||||
if _newline_before_pos() and text[r.pos:].isspace():
|
||||
self.finish = True
|
||||
return
|
||||
|
||||
# auto-indent the next line like the previous line
|
||||
prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos)
|
||||
r.insert("\n")
|
||||
if not self.reader.paste_mode:
|
||||
if indent:
|
||||
for i in range(prevlinestart, prevlinestart + indent):
|
||||
r.insert(r.buffer[i])
|
||||
r.update_last_used_indentation()
|
||||
if _should_auto_indent(r.buffer, r.pos):
|
||||
if r.last_used_indentation is not None:
|
||||
indentation = r.last_used_indentation
|
||||
else:
|
||||
# default
|
||||
indentation = " " * 4
|
||||
r.insert(indentation)
|
||||
elif not self.reader.paste_mode:
|
||||
self.finish = True
|
||||
else:
|
||||
r.insert("\n")
|
||||
|
||||
|
||||
class backspace_dedent(commands.Command):
|
||||
def do(self) -> None:
|
||||
r = self.reader
|
||||
b = r.buffer
|
||||
if r.pos > 0:
|
||||
repeat = 1
|
||||
if b[r.pos - 1] != "\n":
|
||||
indent = _get_this_line_indent(b, r.pos)
|
||||
if indent > 0:
|
||||
ls = r.pos - indent
|
||||
while ls > 0:
|
||||
ls, pi = _get_previous_line_indent(b, ls - 1)
|
||||
if pi is not None and pi < indent:
|
||||
repeat = indent - pi
|
||||
break
|
||||
r.pos -= repeat
|
||||
del b[r.pos : r.pos + repeat]
|
||||
r.dirty = True
|
||||
else:
|
||||
self.reader.error("can't backspace at start")
|
||||
|
||||
|
||||
# ____________________________________________________________
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _ReadlineWrapper:
|
||||
f_in: int = -1
|
||||
f_out: int = -1
|
||||
reader: ReadlineAlikeReader | None = field(default=None, repr=False)
|
||||
saved_history_length: int = -1
|
||||
startup_hook: Callback | None = None
|
||||
config: ReadlineConfig = field(default_factory=ReadlineConfig, repr=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.f_in == -1:
|
||||
self.f_in = os.dup(0)
|
||||
if self.f_out == -1:
|
||||
self.f_out = os.dup(1)
|
||||
|
||||
def get_reader(self) -> ReadlineAlikeReader:
|
||||
if self.reader is None:
|
||||
console = Console(self.f_in, self.f_out, encoding=ENCODING)
|
||||
self.reader = ReadlineAlikeReader(console=console, config=self.config)
|
||||
return self.reader
|
||||
|
||||
def input(self, prompt: object = "") -> str:
|
||||
try:
|
||||
reader = self.get_reader()
|
||||
except _error:
|
||||
assert raw_input is not None
|
||||
return raw_input(prompt)
|
||||
prompt_str = str(prompt)
|
||||
reader.ps1 = prompt_str
|
||||
sys.audit("builtins.input", prompt_str)
|
||||
result = reader.readline(startup_hook=self.startup_hook)
|
||||
sys.audit("builtins.input/result", result)
|
||||
return result
|
||||
|
||||
def multiline_input(self, more_lines: MoreLinesCallable, ps1: str, ps2: str) -> str:
|
||||
"""Read an input on possibly multiple lines, asking for more
|
||||
lines as long as 'more_lines(unicodetext)' returns an object whose
|
||||
boolean value is true.
|
||||
"""
|
||||
reader = self.get_reader()
|
||||
saved = reader.more_lines
|
||||
try:
|
||||
reader.more_lines = more_lines
|
||||
reader.ps1 = ps1
|
||||
reader.ps2 = ps1
|
||||
reader.ps3 = ps2
|
||||
reader.ps4 = ""
|
||||
with warnings.catch_warnings(action="ignore"):
|
||||
return reader.readline()
|
||||
finally:
|
||||
reader.more_lines = saved
|
||||
reader.paste_mode = False
|
||||
|
||||
def parse_and_bind(self, string: str) -> None:
|
||||
pass # XXX we don't support parsing GNU-readline-style init files
|
||||
|
||||
def set_completer(self, function: Completer | None = None) -> None:
|
||||
self.config.readline_completer = function
|
||||
|
||||
def get_completer(self) -> Completer | None:
|
||||
return self.config.readline_completer
|
||||
|
||||
def set_completer_delims(self, delimiters: Collection[str]) -> None:
|
||||
self.config.completer_delims = frozenset(delimiters)
|
||||
|
||||
def get_completer_delims(self) -> str:
|
||||
return "".join(sorted(self.config.completer_delims))
|
||||
|
||||
def _histline(self, line: str) -> str:
|
||||
line = line.rstrip("\n")
|
||||
return line
|
||||
|
||||
def get_history_length(self) -> int:
|
||||
return self.saved_history_length
|
||||
|
||||
def set_history_length(self, length: int) -> None:
|
||||
self.saved_history_length = length
|
||||
|
||||
def get_current_history_length(self) -> int:
|
||||
return len(self.get_reader().history)
|
||||
|
||||
def read_history_file(self, filename: str = gethistoryfile()) -> None:
|
||||
# multiline extension (really a hack) for the end of lines that
|
||||
# are actually continuations inside a single multiline_input()
|
||||
# history item: we use \r\n instead of just \n. If the history
|
||||
# file is passed to GNU readline, the extra \r are just ignored.
|
||||
history = self.get_reader().history
|
||||
|
||||
with open(os.path.expanduser(filename), 'rb') as f:
|
||||
is_editline = f.readline().startswith(b"_HiStOrY_V2_")
|
||||
if is_editline:
|
||||
encoding = "unicode-escape"
|
||||
else:
|
||||
f.seek(0)
|
||||
encoding = "utf-8"
|
||||
|
||||
lines = [line.decode(encoding, errors='replace') for line in f.read().split(b'\n')]
|
||||
buffer = []
|
||||
for line in lines:
|
||||
if line.endswith("\r"):
|
||||
buffer.append(line+'\n')
|
||||
else:
|
||||
line = self._histline(line)
|
||||
if buffer:
|
||||
line = self._histline("".join(buffer).replace("\r", "") + line)
|
||||
del buffer[:]
|
||||
if line:
|
||||
history.append(line)
|
||||
|
||||
def write_history_file(self, filename: str = gethistoryfile()) -> None:
|
||||
maxlength = self.saved_history_length
|
||||
history = self.get_reader().get_trimmed_history(maxlength)
|
||||
f = open(os.path.expanduser(filename), "w",
|
||||
encoding="utf-8", newline="\n")
|
||||
with f:
|
||||
for entry in history:
|
||||
entry = entry.replace("\n", "\r\n") # multiline history support
|
||||
f.write(entry + "\n")
|
||||
|
||||
def clear_history(self) -> None:
|
||||
del self.get_reader().history[:]
|
||||
|
||||
def get_history_item(self, index: int) -> str | None:
|
||||
history = self.get_reader().history
|
||||
if 1 <= index <= len(history):
|
||||
return history[index - 1]
|
||||
else:
|
||||
return None # like readline.c
|
||||
|
||||
def remove_history_item(self, index: int) -> None:
|
||||
history = self.get_reader().history
|
||||
if 0 <= index < len(history):
|
||||
del history[index]
|
||||
else:
|
||||
raise ValueError("No history item at position %d" % index)
|
||||
# like readline.c
|
||||
|
||||
def replace_history_item(self, index: int, line: str) -> None:
|
||||
history = self.get_reader().history
|
||||
if 0 <= index < len(history):
|
||||
history[index] = self._histline(line)
|
||||
else:
|
||||
raise ValueError("No history item at position %d" % index)
|
||||
# like readline.c
|
||||
|
||||
def add_history(self, line: str) -> None:
|
||||
self.get_reader().history.append(self._histline(line))
|
||||
|
||||
def set_startup_hook(self, function: Callback | None = None) -> None:
|
||||
self.startup_hook = function
|
||||
|
||||
def get_line_buffer(self) -> str:
|
||||
return self.get_reader().get_unicode()
|
||||
|
||||
def _get_idxs(self) -> tuple[int, int]:
|
||||
start = cursor = self.get_reader().pos
|
||||
buf = self.get_line_buffer()
|
||||
for i in range(cursor - 1, -1, -1):
|
||||
if buf[i] in self.get_completer_delims():
|
||||
break
|
||||
start = i
|
||||
return start, cursor
|
||||
|
||||
def get_begidx(self) -> int:
|
||||
return self._get_idxs()[0]
|
||||
|
||||
def get_endidx(self) -> int:
|
||||
return self._get_idxs()[1]
|
||||
|
||||
def insert_text(self, text: str) -> None:
|
||||
self.get_reader().insert(text)
|
||||
|
||||
|
||||
_wrapper = _ReadlineWrapper()
|
||||
|
||||
# ____________________________________________________________
|
||||
# Public API
|
||||
|
||||
parse_and_bind = _wrapper.parse_and_bind
|
||||
set_completer = _wrapper.set_completer
|
||||
get_completer = _wrapper.get_completer
|
||||
set_completer_delims = _wrapper.set_completer_delims
|
||||
get_completer_delims = _wrapper.get_completer_delims
|
||||
get_history_length = _wrapper.get_history_length
|
||||
set_history_length = _wrapper.set_history_length
|
||||
get_current_history_length = _wrapper.get_current_history_length
|
||||
read_history_file = _wrapper.read_history_file
|
||||
write_history_file = _wrapper.write_history_file
|
||||
clear_history = _wrapper.clear_history
|
||||
get_history_item = _wrapper.get_history_item
|
||||
remove_history_item = _wrapper.remove_history_item
|
||||
replace_history_item = _wrapper.replace_history_item
|
||||
add_history = _wrapper.add_history
|
||||
set_startup_hook = _wrapper.set_startup_hook
|
||||
get_line_buffer = _wrapper.get_line_buffer
|
||||
get_begidx = _wrapper.get_begidx
|
||||
get_endidx = _wrapper.get_endidx
|
||||
insert_text = _wrapper.insert_text
|
||||
|
||||
# Extension
|
||||
multiline_input = _wrapper.multiline_input
|
||||
|
||||
# Internal hook
|
||||
_get_reader = _wrapper.get_reader
|
||||
|
||||
# ____________________________________________________________
|
||||
# Stubs
|
||||
|
||||
|
||||
def _make_stub(_name: str, _ret: object) -> None:
|
||||
def stub(*args: object, **kwds: object) -> None:
|
||||
import warnings
|
||||
|
||||
warnings.warn("readline.%s() not implemented" % _name, stacklevel=2)
|
||||
|
||||
stub.__name__ = _name
|
||||
globals()[_name] = stub
|
||||
|
||||
|
||||
for _name, _ret in [
|
||||
("read_init_file", None),
|
||||
("redisplay", None),
|
||||
("set_pre_input_hook", None),
|
||||
]:
|
||||
assert _name not in globals(), _name
|
||||
_make_stub(_name, _ret)
|
||||
|
||||
# ____________________________________________________________
|
||||
|
||||
|
||||
def _setup(namespace: Mapping[str, Any]) -> None:
|
||||
global raw_input
|
||||
if raw_input is not None:
|
||||
return # don't run _setup twice
|
||||
|
||||
try:
|
||||
f_in = sys.stdin.fileno()
|
||||
f_out = sys.stdout.fileno()
|
||||
except (AttributeError, ValueError):
|
||||
return
|
||||
if not os.isatty(f_in) or not os.isatty(f_out):
|
||||
return
|
||||
|
||||
_wrapper.f_in = f_in
|
||||
_wrapper.f_out = f_out
|
||||
|
||||
# set up namespace in rlcompleter, which requires it to be a bona fide dict
|
||||
if not isinstance(namespace, dict):
|
||||
namespace = dict(namespace)
|
||||
_wrapper.config.readline_completer = RLCompleter(namespace).complete
|
||||
|
||||
# this is not really what readline.c does. Better than nothing I guess
|
||||
import builtins
|
||||
raw_input = builtins.input
|
||||
builtins.input = _wrapper.input
|
||||
|
||||
|
||||
raw_input: Callable[[object], str] | None = None
|
||||
167
Lib/_pyrepl/simple_interact.py
vendored
Normal file
167
Lib/_pyrepl/simple_interact.py
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
"""This is an alternative to python_reader which tries to emulate
|
||||
the CPython prompt as closely as possible, with the exception of
|
||||
allowing multiline input and multiline history entries.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import _sitebuiltins
|
||||
import linecache
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import code
|
||||
|
||||
from .readline import _get_reader, multiline_input
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
_error: tuple[type[Exception], ...] | type[Exception]
|
||||
try:
|
||||
from .unix_console import _error
|
||||
except ModuleNotFoundError:
|
||||
from .windows_console import _error
|
||||
|
||||
def check() -> str:
|
||||
"""Returns the error message if there is a problem initializing the state."""
|
||||
try:
|
||||
_get_reader()
|
||||
except _error as e:
|
||||
if term := os.environ.get("TERM", ""):
|
||||
term = f"; TERM={term}"
|
||||
return str(str(e) or repr(e) or "unknown error") + term
|
||||
return ""
|
||||
|
||||
|
||||
def _strip_final_indent(text: str) -> str:
|
||||
# kill spaces and tabs at the end, but only if they follow '\n'.
|
||||
# meant to remove the auto-indentation only (although it would of
|
||||
# course also remove explicitly-added indentation).
|
||||
short = text.rstrip(" \t")
|
||||
n = len(short)
|
||||
if n > 0 and text[n - 1] == "\n":
|
||||
return short
|
||||
return text
|
||||
|
||||
|
||||
def _clear_screen():
|
||||
reader = _get_reader()
|
||||
reader.scheduled_commands.append("clear_screen")
|
||||
|
||||
|
||||
REPL_COMMANDS = {
|
||||
"exit": _sitebuiltins.Quitter('exit', ''),
|
||||
"quit": _sitebuiltins.Quitter('quit' ,''),
|
||||
"copyright": _sitebuiltins._Printer('copyright', sys.copyright),
|
||||
"help": _sitebuiltins._Helper(),
|
||||
"clear": _clear_screen,
|
||||
"\x1a": _sitebuiltins.Quitter('\x1a', ''),
|
||||
}
|
||||
|
||||
|
||||
def _more_lines(console: code.InteractiveConsole, unicodetext: str) -> bool:
|
||||
# ooh, look at the hack:
|
||||
src = _strip_final_indent(unicodetext)
|
||||
try:
|
||||
code = console.compile(src, "<stdin>", "single")
|
||||
except (OverflowError, SyntaxError, ValueError):
|
||||
lines = src.splitlines(keepends=True)
|
||||
if len(lines) == 1:
|
||||
return False
|
||||
|
||||
last_line = lines[-1]
|
||||
was_indented = last_line.startswith((" ", "\t"))
|
||||
not_empty = last_line.strip() != ""
|
||||
incomplete = not last_line.endswith("\n")
|
||||
return (was_indented or not_empty) and incomplete
|
||||
else:
|
||||
return code is None
|
||||
|
||||
|
||||
def run_multiline_interactive_console(
|
||||
console: code.InteractiveConsole,
|
||||
*,
|
||||
future_flags: int = 0,
|
||||
) -> None:
|
||||
from .readline import _setup
|
||||
_setup(console.locals)
|
||||
if future_flags:
|
||||
console.compile.compiler.flags |= future_flags
|
||||
|
||||
more_lines = functools.partial(_more_lines, console)
|
||||
input_n = 0
|
||||
|
||||
def maybe_run_command(statement: str) -> bool:
|
||||
statement = statement.strip()
|
||||
if statement in console.locals or statement not in REPL_COMMANDS:
|
||||
return False
|
||||
|
||||
reader = _get_reader()
|
||||
reader.history.pop() # skip internal commands in history
|
||||
command = REPL_COMMANDS[statement]
|
||||
if callable(command):
|
||||
# Make sure that history does not change because of commands
|
||||
with reader.suspend_history():
|
||||
command()
|
||||
return True
|
||||
return False
|
||||
|
||||
while 1:
|
||||
try:
|
||||
try:
|
||||
sys.stdout.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ps1 = getattr(sys, "ps1", ">>> ")
|
||||
ps2 = getattr(sys, "ps2", "... ")
|
||||
try:
|
||||
statement = multiline_input(more_lines, ps1, ps2)
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
if maybe_run_command(statement):
|
||||
continue
|
||||
|
||||
input_name = f"<python-input-{input_n}>"
|
||||
linecache._register_code(input_name, statement, "<stdin>") # type: ignore[attr-defined]
|
||||
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
|
||||
assert not more
|
||||
input_n += 1
|
||||
except KeyboardInterrupt:
|
||||
r = _get_reader()
|
||||
if r.input_trans is r.isearch_trans:
|
||||
r.do_cmd(("isearch-end", [""]))
|
||||
r.pos = len(r.get_unicode())
|
||||
r.dirty = True
|
||||
r.refresh()
|
||||
r.in_bracketed_paste = False
|
||||
console.write("\nKeyboardInterrupt\n")
|
||||
console.resetbuffer()
|
||||
except MemoryError:
|
||||
console.write("\nMemoryError\n")
|
||||
console.resetbuffer()
|
||||
21
Lib/_pyrepl/trace.py
vendored
Normal file
21
Lib/_pyrepl/trace.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
# types
|
||||
if False:
|
||||
from typing import IO
|
||||
|
||||
|
||||
trace_file: IO[str] | None = None
|
||||
if trace_filename := os.environ.get("PYREPL_TRACE"):
|
||||
trace_file = open(trace_filename, "a")
|
||||
|
||||
|
||||
def trace(line: str, *k: object, **kw: object) -> None:
|
||||
if trace_file is None:
|
||||
return
|
||||
if k or kw:
|
||||
line = line.format(*k, **kw)
|
||||
trace_file.write(line + "\n")
|
||||
trace_file.flush()
|
||||
8
Lib/_pyrepl/types.py
vendored
Normal file
8
Lib/_pyrepl/types.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
from collections.abc import Callable, Iterator
|
||||
|
||||
Callback = Callable[[], object]
|
||||
SimpleContextManager = Iterator[None]
|
||||
KeySpec = str # like r"\C-c"
|
||||
CommandName = str # like "interrupt"
|
||||
EventTuple = tuple[CommandName, str]
|
||||
Completer = Callable[[str, int], str | None]
|
||||
810
Lib/_pyrepl/unix_console.py
vendored
Normal file
810
Lib/_pyrepl/unix_console.py
vendored
Normal file
@@ -0,0 +1,810 @@
|
||||
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Antonio Cuni
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import signal
|
||||
import struct
|
||||
import termios
|
||||
import time
|
||||
import platform
|
||||
from fcntl import ioctl
|
||||
|
||||
from . import curses
|
||||
from .console import Console, Event
|
||||
from .fancy_termios import tcgetattr, tcsetattr
|
||||
from .trace import trace
|
||||
from .unix_eventqueue import EventQueue
|
||||
from .utils import wlen
|
||||
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
# types
|
||||
if TYPE_CHECKING:
|
||||
from typing import IO, Literal, overload
|
||||
else:
|
||||
overload = lambda func: None
|
||||
|
||||
|
||||
class InvalidTerminal(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
_error = (termios.error, curses.error, InvalidTerminal)
|
||||
|
||||
SIGWINCH_EVENT = "repaint"
|
||||
|
||||
FIONREAD = getattr(termios, "FIONREAD", None)
|
||||
TIOCGWINSZ = getattr(termios, "TIOCGWINSZ", None)
|
||||
|
||||
# ------------ start of baudrate definitions ------------
|
||||
|
||||
# Add (possibly) missing baudrates (check termios man page) to termios
|
||||
|
||||
|
||||
def add_baudrate_if_supported(dictionary: dict[int, int], rate: int) -> None:
|
||||
baudrate_name = "B%d" % rate
|
||||
if hasattr(termios, baudrate_name):
|
||||
dictionary[getattr(termios, baudrate_name)] = rate
|
||||
|
||||
|
||||
# Check the termios man page (Line speed) to know where these
|
||||
# values come from.
|
||||
potential_baudrates = [
|
||||
0,
|
||||
110,
|
||||
115200,
|
||||
1200,
|
||||
134,
|
||||
150,
|
||||
1800,
|
||||
19200,
|
||||
200,
|
||||
230400,
|
||||
2400,
|
||||
300,
|
||||
38400,
|
||||
460800,
|
||||
4800,
|
||||
50,
|
||||
57600,
|
||||
600,
|
||||
75,
|
||||
9600,
|
||||
]
|
||||
|
||||
ratedict: dict[int, int] = {}
|
||||
for rate in potential_baudrates:
|
||||
add_baudrate_if_supported(ratedict, rate)
|
||||
|
||||
# Clean up variables to avoid unintended usage
|
||||
del rate, add_baudrate_if_supported
|
||||
|
||||
# ------------ end of baudrate definitions ------------
|
||||
|
||||
delayprog = re.compile(b"\\$<([0-9]+)((?:/|\\*){0,2})>")
|
||||
|
||||
try:
|
||||
poll: type[select.poll] = select.poll
|
||||
except AttributeError:
|
||||
# this is exactly the minumum necessary to support what we
|
||||
# do with poll objects
|
||||
class MinimalPoll:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def register(self, fd, flag):
|
||||
self.fd = fd
|
||||
# note: The 'timeout' argument is received as *milliseconds*
|
||||
def poll(self, timeout: float | None = None) -> list[int]:
|
||||
if timeout is None:
|
||||
r, w, e = select.select([self.fd], [], [])
|
||||
else:
|
||||
r, w, e = select.select([self.fd], [], [], timeout/1000)
|
||||
return r
|
||||
|
||||
poll = MinimalPoll # type: ignore[assignment]
|
||||
|
||||
|
||||
class UnixConsole(Console):
|
||||
def __init__(
|
||||
self,
|
||||
f_in: IO[bytes] | int = 0,
|
||||
f_out: IO[bytes] | int = 1,
|
||||
term: str = "",
|
||||
encoding: str = "",
|
||||
):
|
||||
"""
|
||||
Initialize the UnixConsole.
|
||||
|
||||
Parameters:
|
||||
- f_in (int or file-like object): Input file descriptor or object.
|
||||
- f_out (int or file-like object): Output file descriptor or object.
|
||||
- term (str): Terminal name.
|
||||
- encoding (str): Encoding to use for I/O operations.
|
||||
"""
|
||||
super().__init__(f_in, f_out, term, encoding)
|
||||
|
||||
self.pollob = poll()
|
||||
self.pollob.register(self.input_fd, select.POLLIN)
|
||||
self.input_buffer = b""
|
||||
self.input_buffer_pos = 0
|
||||
curses.setupterm(term or None, self.output_fd)
|
||||
self.term = term
|
||||
|
||||
@overload
|
||||
def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: ...
|
||||
|
||||
@overload
|
||||
def _my_getstr(cap: str, optional: bool) -> bytes | None: ...
|
||||
|
||||
def _my_getstr(cap: str, optional: bool = False) -> bytes | None:
|
||||
r = curses.tigetstr(cap)
|
||||
if not optional and r is None:
|
||||
raise InvalidTerminal(
|
||||
f"terminal doesn't have the required {cap} capability"
|
||||
)
|
||||
return r
|
||||
|
||||
self._bel = _my_getstr("bel")
|
||||
self._civis = _my_getstr("civis", optional=True)
|
||||
self._clear = _my_getstr("clear")
|
||||
self._cnorm = _my_getstr("cnorm", optional=True)
|
||||
self._cub = _my_getstr("cub", optional=True)
|
||||
self._cub1 = _my_getstr("cub1", optional=True)
|
||||
self._cud = _my_getstr("cud", optional=True)
|
||||
self._cud1 = _my_getstr("cud1", optional=True)
|
||||
self._cuf = _my_getstr("cuf", optional=True)
|
||||
self._cuf1 = _my_getstr("cuf1", optional=True)
|
||||
self._cup = _my_getstr("cup")
|
||||
self._cuu = _my_getstr("cuu", optional=True)
|
||||
self._cuu1 = _my_getstr("cuu1", optional=True)
|
||||
self._dch1 = _my_getstr("dch1", optional=True)
|
||||
self._dch = _my_getstr("dch", optional=True)
|
||||
self._el = _my_getstr("el")
|
||||
self._hpa = _my_getstr("hpa", optional=True)
|
||||
self._ich = _my_getstr("ich", optional=True)
|
||||
self._ich1 = _my_getstr("ich1", optional=True)
|
||||
self._ind = _my_getstr("ind", optional=True)
|
||||
self._pad = _my_getstr("pad", optional=True)
|
||||
self._ri = _my_getstr("ri", optional=True)
|
||||
self._rmkx = _my_getstr("rmkx", optional=True)
|
||||
self._smkx = _my_getstr("smkx", optional=True)
|
||||
|
||||
self.__setup_movement()
|
||||
|
||||
self.event_queue = EventQueue(self.input_fd, self.encoding)
|
||||
self.cursor_visible = 1
|
||||
|
||||
def more_in_buffer(self) -> bool:
|
||||
return bool(
|
||||
self.input_buffer
|
||||
and self.input_buffer_pos < len(self.input_buffer)
|
||||
)
|
||||
|
||||
def __read(self, n: int) -> bytes:
|
||||
if not self.more_in_buffer():
|
||||
self.input_buffer = os.read(self.input_fd, 10000)
|
||||
|
||||
ret = self.input_buffer[self.input_buffer_pos : self.input_buffer_pos + n]
|
||||
self.input_buffer_pos += len(ret)
|
||||
if self.input_buffer_pos >= len(self.input_buffer):
|
||||
self.input_buffer = b""
|
||||
self.input_buffer_pos = 0
|
||||
return ret
|
||||
|
||||
|
||||
def change_encoding(self, encoding: str) -> None:
|
||||
"""
|
||||
Change the encoding used for I/O operations.
|
||||
|
||||
Parameters:
|
||||
- encoding (str): New encoding to use.
|
||||
"""
|
||||
self.encoding = encoding
|
||||
|
||||
def refresh(self, screen, c_xy):
|
||||
"""
|
||||
Refresh the console screen.
|
||||
|
||||
Parameters:
|
||||
- screen (list): List of strings representing the screen contents.
|
||||
- c_xy (tuple): Cursor position (x, y) on the screen.
|
||||
"""
|
||||
cx, cy = c_xy
|
||||
if not self.__gone_tall:
|
||||
while len(self.screen) < min(len(screen), self.height):
|
||||
self.__hide_cursor()
|
||||
self.__move(0, len(self.screen) - 1)
|
||||
self.__write("\n")
|
||||
self.posxy = 0, len(self.screen)
|
||||
self.screen.append("")
|
||||
else:
|
||||
while len(self.screen) < len(screen):
|
||||
self.screen.append("")
|
||||
|
||||
if len(screen) > self.height:
|
||||
self.__gone_tall = 1
|
||||
self.__move = self.__move_tall
|
||||
|
||||
px, py = self.posxy
|
||||
old_offset = offset = self.__offset
|
||||
height = self.height
|
||||
|
||||
# we make sure the cursor is on the screen, and that we're
|
||||
# using all of the screen if we can
|
||||
if cy < offset:
|
||||
offset = cy
|
||||
elif cy >= offset + height:
|
||||
offset = cy - height + 1
|
||||
elif offset > 0 and len(screen) < offset + height:
|
||||
offset = max(len(screen) - height, 0)
|
||||
screen.append("")
|
||||
|
||||
oldscr = self.screen[old_offset : old_offset + height]
|
||||
newscr = screen[offset : offset + height]
|
||||
|
||||
# use hardware scrolling if we have it.
|
||||
if old_offset > offset and self._ri:
|
||||
self.__hide_cursor()
|
||||
self.__write_code(self._cup, 0, 0)
|
||||
self.posxy = 0, old_offset
|
||||
for i in range(old_offset - offset):
|
||||
self.__write_code(self._ri)
|
||||
oldscr.pop(-1)
|
||||
oldscr.insert(0, "")
|
||||
elif old_offset < offset and self._ind:
|
||||
self.__hide_cursor()
|
||||
self.__write_code(self._cup, self.height - 1, 0)
|
||||
self.posxy = 0, old_offset + self.height - 1
|
||||
for i in range(offset - old_offset):
|
||||
self.__write_code(self._ind)
|
||||
oldscr.pop(0)
|
||||
oldscr.append("")
|
||||
|
||||
self.__offset = offset
|
||||
|
||||
for (
|
||||
y,
|
||||
oldline,
|
||||
newline,
|
||||
) in zip(range(offset, offset + height), oldscr, newscr):
|
||||
if oldline != newline:
|
||||
self.__write_changed_line(y, oldline, newline, px)
|
||||
|
||||
y = len(newscr)
|
||||
while y < len(oldscr):
|
||||
self.__hide_cursor()
|
||||
self.__move(0, y)
|
||||
self.posxy = 0, y
|
||||
self.__write_code(self._el)
|
||||
y += 1
|
||||
|
||||
self.__show_cursor()
|
||||
|
||||
self.screen = screen.copy()
|
||||
self.move_cursor(cx, cy)
|
||||
self.flushoutput()
|
||||
|
||||
def move_cursor(self, x, y):
|
||||
"""
|
||||
Move the cursor to the specified position on the screen.
|
||||
|
||||
Parameters:
|
||||
- x (int): X coordinate.
|
||||
- y (int): Y coordinate.
|
||||
"""
|
||||
if y < self.__offset or y >= self.__offset + self.height:
|
||||
self.event_queue.insert(Event("scroll", None))
|
||||
else:
|
||||
self.__move(x, y)
|
||||
self.posxy = x, y
|
||||
self.flushoutput()
|
||||
|
||||
def prepare(self):
|
||||
"""
|
||||
Prepare the console for input/output operations.
|
||||
"""
|
||||
self.__svtermstate = tcgetattr(self.input_fd)
|
||||
raw = self.__svtermstate.copy()
|
||||
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
|
||||
raw.oflag &= ~(termios.OPOST)
|
||||
raw.cflag &= ~(termios.CSIZE | termios.PARENB)
|
||||
raw.cflag |= termios.CS8
|
||||
raw.iflag |= termios.BRKINT
|
||||
raw.lflag &= ~(termios.ICANON | termios.ECHO | termios.IEXTEN)
|
||||
raw.lflag |= termios.ISIG
|
||||
raw.cc[termios.VMIN] = 1
|
||||
raw.cc[termios.VTIME] = 0
|
||||
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
|
||||
|
||||
# In macOS terminal we need to deactivate line wrap via ANSI escape code
|
||||
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
|
||||
os.write(self.output_fd, b"\033[?7l")
|
||||
|
||||
self.screen = []
|
||||
self.height, self.width = self.getheightwidth()
|
||||
|
||||
self.__buffer = []
|
||||
|
||||
self.posxy = 0, 0
|
||||
self.__gone_tall = 0
|
||||
self.__move = self.__move_short
|
||||
self.__offset = 0
|
||||
|
||||
self.__maybe_write_code(self._smkx)
|
||||
|
||||
try:
|
||||
self.old_sigwinch = signal.signal(signal.SIGWINCH, self.__sigwinch)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.__enable_bracketed_paste()
|
||||
|
||||
def restore(self):
|
||||
"""
|
||||
Restore the console to the default state
|
||||
"""
|
||||
self.__disable_bracketed_paste()
|
||||
self.__maybe_write_code(self._rmkx)
|
||||
self.flushoutput()
|
||||
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
|
||||
|
||||
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
|
||||
os.write(self.output_fd, b"\033[?7h")
|
||||
|
||||
if hasattr(self, "old_sigwinch"):
|
||||
signal.signal(signal.SIGWINCH, self.old_sigwinch)
|
||||
del self.old_sigwinch
|
||||
|
||||
def push_char(self, char: int | bytes) -> None:
|
||||
"""
|
||||
Push a character to the console event queue.
|
||||
"""
|
||||
trace("push char {char!r}", char=char)
|
||||
self.event_queue.push(char)
|
||||
|
||||
def get_event(self, block: bool = True) -> Event | None:
|
||||
"""
|
||||
Get an event from the console event queue.
|
||||
|
||||
Parameters:
|
||||
- block (bool): Whether to block until an event is available.
|
||||
|
||||
Returns:
|
||||
- Event: Event object from the event queue.
|
||||
"""
|
||||
if not block and not self.wait(timeout=0):
|
||||
return None
|
||||
|
||||
while self.event_queue.empty():
|
||||
while True:
|
||||
try:
|
||||
self.push_char(self.__read(1))
|
||||
except OSError as err:
|
||||
if err.errno == errno.EINTR:
|
||||
if not self.event_queue.empty():
|
||||
return self.event_queue.get()
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
break
|
||||
return self.event_queue.get()
|
||||
|
||||
def wait(self, timeout: float | None = None) -> bool:
|
||||
"""
|
||||
Wait for events on the console.
|
||||
"""
|
||||
return (
|
||||
not self.event_queue.empty()
|
||||
or self.more_in_buffer()
|
||||
or bool(self.pollob.poll(timeout))
|
||||
)
|
||||
|
||||
def set_cursor_vis(self, visible):
|
||||
"""
|
||||
Set the visibility of the cursor.
|
||||
|
||||
Parameters:
|
||||
- visible (bool): Visibility flag.
|
||||
"""
|
||||
if visible:
|
||||
self.__show_cursor()
|
||||
else:
|
||||
self.__hide_cursor()
|
||||
|
||||
if TIOCGWINSZ:
|
||||
|
||||
def getheightwidth(self):
|
||||
"""
|
||||
Get the height and width of the console.
|
||||
|
||||
Returns:
|
||||
- tuple: Height and width of the console.
|
||||
"""
|
||||
try:
|
||||
return int(os.environ["LINES"]), int(os.environ["COLUMNS"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
try:
|
||||
size = ioctl(self.input_fd, TIOCGWINSZ, b"\000" * 8)
|
||||
except OSError:
|
||||
return 25, 80
|
||||
height, width = struct.unpack("hhhh", size)[0:2]
|
||||
if not height:
|
||||
return 25, 80
|
||||
return height, width
|
||||
|
||||
else:
|
||||
|
||||
def getheightwidth(self):
|
||||
"""
|
||||
Get the height and width of the console.
|
||||
|
||||
Returns:
|
||||
- tuple: Height and width of the console.
|
||||
"""
|
||||
try:
|
||||
return int(os.environ["LINES"]), int(os.environ["COLUMNS"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return 25, 80
|
||||
|
||||
def forgetinput(self):
|
||||
"""
|
||||
Discard any pending input on the console.
|
||||
"""
|
||||
termios.tcflush(self.input_fd, termios.TCIFLUSH)
|
||||
|
||||
def flushoutput(self):
|
||||
"""
|
||||
Flush the output buffer.
|
||||
"""
|
||||
for text, iscode in self.__buffer:
|
||||
if iscode:
|
||||
self.__tputs(text)
|
||||
else:
|
||||
os.write(self.output_fd, text.encode(self.encoding, "replace"))
|
||||
del self.__buffer[:]
|
||||
|
||||
def finish(self):
|
||||
"""
|
||||
Finish console operations and flush the output buffer.
|
||||
"""
|
||||
y = len(self.screen) - 1
|
||||
while y >= 0 and not self.screen[y]:
|
||||
y -= 1
|
||||
self.__move(0, min(y, self.height + self.__offset - 1))
|
||||
self.__write("\n\r")
|
||||
self.flushoutput()
|
||||
|
||||
def beep(self):
|
||||
"""
|
||||
Emit a beep sound.
|
||||
"""
|
||||
self.__maybe_write_code(self._bel)
|
||||
self.flushoutput()
|
||||
|
||||
if FIONREAD:
|
||||
|
||||
def getpending(self):
|
||||
"""
|
||||
Get pending events from the console event queue.
|
||||
|
||||
Returns:
|
||||
- Event: Pending event from the event queue.
|
||||
"""
|
||||
e = Event("key", "", b"")
|
||||
|
||||
while not self.event_queue.empty():
|
||||
e2 = self.event_queue.get()
|
||||
e.data += e2.data
|
||||
e.raw += e.raw
|
||||
|
||||
amount = struct.unpack("i", ioctl(self.input_fd, FIONREAD, b"\0\0\0\0"))[0]
|
||||
raw = self.__read(amount)
|
||||
data = str(raw, self.encoding, "replace")
|
||||
e.data += data
|
||||
e.raw += raw
|
||||
return e
|
||||
|
||||
else:
|
||||
|
||||
def getpending(self):
|
||||
"""
|
||||
Get pending events from the console event queue.
|
||||
|
||||
Returns:
|
||||
- Event: Pending event from the event queue.
|
||||
"""
|
||||
e = Event("key", "", b"")
|
||||
|
||||
while not self.event_queue.empty():
|
||||
e2 = self.event_queue.get()
|
||||
e.data += e2.data
|
||||
e.raw += e.raw
|
||||
|
||||
amount = 10000
|
||||
raw = self.__read(amount)
|
||||
data = str(raw, self.encoding, "replace")
|
||||
e.data += data
|
||||
e.raw += raw
|
||||
return e
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear the console screen.
|
||||
"""
|
||||
self.__write_code(self._clear)
|
||||
self.__gone_tall = 1
|
||||
self.__move = self.__move_tall
|
||||
self.posxy = 0, 0
|
||||
self.screen = []
|
||||
|
||||
@property
|
||||
def input_hook(self):
|
||||
try:
|
||||
import posix
|
||||
except ImportError:
|
||||
return None
|
||||
if posix._is_inputhook_installed():
|
||||
return posix._inputhook
|
||||
|
||||
def __enable_bracketed_paste(self) -> None:
|
||||
os.write(self.output_fd, b"\x1b[?2004h")
|
||||
|
||||
def __disable_bracketed_paste(self) -> None:
|
||||
os.write(self.output_fd, b"\x1b[?2004l")
|
||||
|
||||
def __setup_movement(self):
|
||||
"""
|
||||
Set up the movement functions based on the terminal capabilities.
|
||||
"""
|
||||
if 0 and self._hpa: # hpa don't work in windows telnet :-(
|
||||
self.__move_x = self.__move_x_hpa
|
||||
elif self._cub and self._cuf:
|
||||
self.__move_x = self.__move_x_cub_cuf
|
||||
elif self._cub1 and self._cuf1:
|
||||
self.__move_x = self.__move_x_cub1_cuf1
|
||||
else:
|
||||
raise RuntimeError("insufficient terminal (horizontal)")
|
||||
|
||||
if self._cuu and self._cud:
|
||||
self.__move_y = self.__move_y_cuu_cud
|
||||
elif self._cuu1 and self._cud1:
|
||||
self.__move_y = self.__move_y_cuu1_cud1
|
||||
else:
|
||||
raise RuntimeError("insufficient terminal (vertical)")
|
||||
|
||||
if self._dch1:
|
||||
self.dch1 = self._dch1
|
||||
elif self._dch:
|
||||
self.dch1 = curses.tparm(self._dch, 1)
|
||||
else:
|
||||
self.dch1 = None
|
||||
|
||||
if self._ich1:
|
||||
self.ich1 = self._ich1
|
||||
elif self._ich:
|
||||
self.ich1 = curses.tparm(self._ich, 1)
|
||||
else:
|
||||
self.ich1 = None
|
||||
|
||||
self.__move = self.__move_short
|
||||
|
||||
def __write_changed_line(self, y, oldline, newline, px_coord):
|
||||
# this is frustrating; there's no reason to test (say)
|
||||
# self.dch1 inside the loop -- but alternative ways of
|
||||
# structuring this function are equally painful (I'm trying to
|
||||
# avoid writing code generators these days...)
|
||||
minlen = min(wlen(oldline), wlen(newline))
|
||||
x_pos = 0
|
||||
x_coord = 0
|
||||
|
||||
px_pos = 0
|
||||
j = 0
|
||||
for c in oldline:
|
||||
if j >= px_coord:
|
||||
break
|
||||
j += wlen(c)
|
||||
px_pos += 1
|
||||
|
||||
# reuse the oldline as much as possible, but stop as soon as we
|
||||
# encounter an ESCAPE, because it might be the start of an escape
|
||||
# sequene
|
||||
while (
|
||||
x_coord < minlen
|
||||
and oldline[x_pos] == newline[x_pos]
|
||||
and newline[x_pos] != "\x1b"
|
||||
):
|
||||
x_coord += wlen(newline[x_pos])
|
||||
x_pos += 1
|
||||
|
||||
# if we need to insert a single character right after the first detected change
|
||||
if oldline[x_pos:] == newline[x_pos + 1 :] and self.ich1:
|
||||
if (
|
||||
y == self.posxy[1]
|
||||
and x_coord > self.posxy[0]
|
||||
and oldline[px_pos:x_pos] == newline[px_pos + 1 : x_pos + 1]
|
||||
):
|
||||
x_pos = px_pos
|
||||
x_coord = px_coord
|
||||
character_width = wlen(newline[x_pos])
|
||||
self.__move(x_coord, y)
|
||||
self.__write_code(self.ich1)
|
||||
self.__write(newline[x_pos])
|
||||
self.posxy = x_coord + character_width, y
|
||||
|
||||
# if it's a single character change in the middle of the line
|
||||
elif (
|
||||
x_coord < minlen
|
||||
and oldline[x_pos + 1 :] == newline[x_pos + 1 :]
|
||||
and wlen(oldline[x_pos]) == wlen(newline[x_pos])
|
||||
):
|
||||
character_width = wlen(newline[x_pos])
|
||||
self.__move(x_coord, y)
|
||||
self.__write(newline[x_pos])
|
||||
self.posxy = x_coord + character_width, y
|
||||
|
||||
# if this is the last character to fit in the line and we edit in the middle of the line
|
||||
elif (
|
||||
self.dch1
|
||||
and self.ich1
|
||||
and wlen(newline) == self.width
|
||||
and x_coord < wlen(newline) - 2
|
||||
and newline[x_pos + 1 : -1] == oldline[x_pos:-2]
|
||||
):
|
||||
self.__hide_cursor()
|
||||
self.__move(self.width - 2, y)
|
||||
self.posxy = self.width - 2, y
|
||||
self.__write_code(self.dch1)
|
||||
|
||||
character_width = wlen(newline[x_pos])
|
||||
self.__move(x_coord, y)
|
||||
self.__write_code(self.ich1)
|
||||
self.__write(newline[x_pos])
|
||||
self.posxy = character_width + 1, y
|
||||
|
||||
else:
|
||||
self.__hide_cursor()
|
||||
self.__move(x_coord, y)
|
||||
if wlen(oldline) > wlen(newline):
|
||||
self.__write_code(self._el)
|
||||
self.__write(newline[x_pos:])
|
||||
self.posxy = wlen(newline), y
|
||||
|
||||
if "\x1b" in newline:
|
||||
# ANSI escape characters are present, so we can't assume
|
||||
# anything about the position of the cursor. Moving the cursor
|
||||
# to the left margin should work to get to a known position.
|
||||
self.move_cursor(0, y)
|
||||
|
||||
def __write(self, text):
|
||||
self.__buffer.append((text, 0))
|
||||
|
||||
def __write_code(self, fmt, *args):
|
||||
self.__buffer.append((curses.tparm(fmt, *args), 1))
|
||||
|
||||
def __maybe_write_code(self, fmt, *args):
|
||||
if fmt:
|
||||
self.__write_code(fmt, *args)
|
||||
|
||||
def __move_y_cuu1_cud1(self, y):
|
||||
assert self._cud1 is not None
|
||||
assert self._cuu1 is not None
|
||||
dy = y - self.posxy[1]
|
||||
if dy > 0:
|
||||
self.__write_code(dy * self._cud1)
|
||||
elif dy < 0:
|
||||
self.__write_code((-dy) * self._cuu1)
|
||||
|
||||
def __move_y_cuu_cud(self, y):
|
||||
dy = y - self.posxy[1]
|
||||
if dy > 0:
|
||||
self.__write_code(self._cud, dy)
|
||||
elif dy < 0:
|
||||
self.__write_code(self._cuu, -dy)
|
||||
|
||||
def __move_x_hpa(self, x: int) -> None:
|
||||
if x != self.posxy[0]:
|
||||
self.__write_code(self._hpa, x)
|
||||
|
||||
def __move_x_cub1_cuf1(self, x: int) -> None:
|
||||
assert self._cuf1 is not None
|
||||
assert self._cub1 is not None
|
||||
dx = x - self.posxy[0]
|
||||
if dx > 0:
|
||||
self.__write_code(self._cuf1 * dx)
|
||||
elif dx < 0:
|
||||
self.__write_code(self._cub1 * (-dx))
|
||||
|
||||
def __move_x_cub_cuf(self, x: int) -> None:
|
||||
dx = x - self.posxy[0]
|
||||
if dx > 0:
|
||||
self.__write_code(self._cuf, dx)
|
||||
elif dx < 0:
|
||||
self.__write_code(self._cub, -dx)
|
||||
|
||||
def __move_short(self, x, y):
|
||||
self.__move_x(x)
|
||||
self.__move_y(y)
|
||||
|
||||
def __move_tall(self, x, y):
|
||||
assert 0 <= y - self.__offset < self.height, y - self.__offset
|
||||
self.__write_code(self._cup, y - self.__offset, x)
|
||||
|
||||
def __sigwinch(self, signum, frame):
|
||||
self.height, self.width = self.getheightwidth()
|
||||
self.event_queue.insert(Event("resize", None))
|
||||
|
||||
def __hide_cursor(self):
|
||||
if self.cursor_visible:
|
||||
self.__maybe_write_code(self._civis)
|
||||
self.cursor_visible = 0
|
||||
|
||||
def __show_cursor(self):
|
||||
if not self.cursor_visible:
|
||||
self.__maybe_write_code(self._cnorm)
|
||||
self.cursor_visible = 1
|
||||
|
||||
def repaint(self):
|
||||
if not self.__gone_tall:
|
||||
self.posxy = 0, self.posxy[1]
|
||||
self.__write("\r")
|
||||
ns = len(self.screen) * ["\000" * self.width]
|
||||
self.screen = ns
|
||||
else:
|
||||
self.posxy = 0, self.__offset
|
||||
self.__move(0, self.__offset)
|
||||
ns = self.height * ["\000" * self.width]
|
||||
self.screen = ns
|
||||
|
||||
def __tputs(self, fmt, prog=delayprog):
|
||||
"""A Python implementation of the curses tputs function; the
|
||||
curses one can't really be wrapped in a sane manner.
|
||||
|
||||
I have the strong suspicion that this is complexity that
|
||||
will never do anyone any good."""
|
||||
# using .get() means that things will blow up
|
||||
# only if the bps is actually needed (which I'm
|
||||
# betting is pretty unlkely)
|
||||
bps = ratedict.get(self.__svtermstate.ospeed)
|
||||
while 1:
|
||||
m = prog.search(fmt)
|
||||
if not m:
|
||||
os.write(self.output_fd, fmt)
|
||||
break
|
||||
x, y = m.span()
|
||||
os.write(self.output_fd, fmt[:x])
|
||||
fmt = fmt[y:]
|
||||
delay = int(m.group(1))
|
||||
if b"*" in m.group(2):
|
||||
delay *= self.height
|
||||
if self._pad and bps is not None:
|
||||
nchars = (bps * delay) / 1000
|
||||
os.write(self.output_fd, self._pad * nchars)
|
||||
else:
|
||||
time.sleep(float(delay) / 1000.0)
|
||||
152
Lib/_pyrepl/unix_eventqueue.py
vendored
Normal file
152
Lib/_pyrepl/unix_eventqueue.py
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
# Copyright 2000-2008 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
# Armin Rigo
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from collections import deque
|
||||
|
||||
from . import keymap
|
||||
from .console import Event
|
||||
from . import curses
|
||||
from .trace import trace
|
||||
from termios import tcgetattr, VERASE
|
||||
import os
|
||||
|
||||
|
||||
# Mapping of human-readable key names to their terminal-specific codes
|
||||
TERMINAL_KEYNAMES = {
|
||||
"delete": "kdch1",
|
||||
"down": "kcud1",
|
||||
"end": "kend",
|
||||
"enter": "kent",
|
||||
"home": "khome",
|
||||
"insert": "kich1",
|
||||
"left": "kcub1",
|
||||
"page down": "knp",
|
||||
"page up": "kpp",
|
||||
"right": "kcuf1",
|
||||
"up": "kcuu1",
|
||||
}
|
||||
|
||||
|
||||
# Function keys F1-F20 mapping
|
||||
TERMINAL_KEYNAMES.update(("f%d" % i, "kf%d" % i) for i in range(1, 21))
|
||||
|
||||
# Known CTRL-arrow keycodes
|
||||
CTRL_ARROW_KEYCODES= {
|
||||
# for xterm, gnome-terminal, xfce terminal, etc.
|
||||
b'\033[1;5D': 'ctrl left',
|
||||
b'\033[1;5C': 'ctrl right',
|
||||
# for rxvt
|
||||
b'\033Od': 'ctrl left',
|
||||
b'\033Oc': 'ctrl right',
|
||||
}
|
||||
|
||||
def get_terminal_keycodes() -> dict[bytes, str]:
|
||||
"""
|
||||
Generates a dictionary mapping terminal keycodes to human-readable names.
|
||||
"""
|
||||
keycodes = {}
|
||||
for key, terminal_code in TERMINAL_KEYNAMES.items():
|
||||
keycode = curses.tigetstr(terminal_code)
|
||||
trace('key {key} tiname {terminal_code} keycode {keycode!r}', **locals())
|
||||
if keycode:
|
||||
keycodes[keycode] = key
|
||||
keycodes.update(CTRL_ARROW_KEYCODES)
|
||||
return keycodes
|
||||
|
||||
class EventQueue:
|
||||
def __init__(self, fd: int, encoding: str) -> None:
|
||||
self.keycodes = get_terminal_keycodes()
|
||||
if os.isatty(fd):
|
||||
backspace = tcgetattr(fd)[6][VERASE]
|
||||
self.keycodes[backspace] = "backspace"
|
||||
self.compiled_keymap = keymap.compile_keymap(self.keycodes)
|
||||
self.keymap = self.compiled_keymap
|
||||
trace("keymap {k!r}", k=self.keymap)
|
||||
self.encoding = encoding
|
||||
self.events: deque[Event] = deque()
|
||||
self.buf = bytearray()
|
||||
|
||||
def get(self) -> Event | None:
|
||||
"""
|
||||
Retrieves the next event from the queue.
|
||||
"""
|
||||
if self.events:
|
||||
return self.events.popleft()
|
||||
else:
|
||||
return None
|
||||
|
||||
def empty(self) -> bool:
|
||||
"""
|
||||
Checks if the queue is empty.
|
||||
"""
|
||||
return not self.events
|
||||
|
||||
def flush_buf(self) -> bytearray:
|
||||
"""
|
||||
Flushes the buffer and returns its contents.
|
||||
"""
|
||||
old = self.buf
|
||||
self.buf = bytearray()
|
||||
return old
|
||||
|
||||
def insert(self, event: Event) -> None:
|
||||
"""
|
||||
Inserts an event into the queue.
|
||||
"""
|
||||
trace('added event {event}', event=event)
|
||||
self.events.append(event)
|
||||
|
||||
def push(self, char: int | bytes) -> None:
|
||||
"""
|
||||
Processes a character by updating the buffer and handling special key mappings.
|
||||
"""
|
||||
ord_char = char if isinstance(char, int) else ord(char)
|
||||
char = bytes(bytearray((ord_char,)))
|
||||
self.buf.append(ord_char)
|
||||
if char in self.keymap:
|
||||
if self.keymap is self.compiled_keymap:
|
||||
#sanity check, buffer is empty when a special key comes
|
||||
assert len(self.buf) == 1
|
||||
k = self.keymap[char]
|
||||
trace('found map {k!r}', k=k)
|
||||
if isinstance(k, dict):
|
||||
self.keymap = k
|
||||
else:
|
||||
self.insert(Event('key', k, self.flush_buf()))
|
||||
self.keymap = self.compiled_keymap
|
||||
|
||||
elif self.buf and self.buf[0] == 27: # escape
|
||||
# escape sequence not recognized by our keymap: propagate it
|
||||
# outside so that i can be recognized as an M-... key (see also
|
||||
# the docstring in keymap.py
|
||||
trace('unrecognized escape sequence, propagating...')
|
||||
self.keymap = self.compiled_keymap
|
||||
self.insert(Event('key', '\033', bytearray(b'\033')))
|
||||
for _c in self.flush_buf()[1:]:
|
||||
self.push(_c)
|
||||
|
||||
else:
|
||||
try:
|
||||
decoded = bytes(self.buf).decode(self.encoding)
|
||||
except UnicodeError:
|
||||
return
|
||||
else:
|
||||
self.insert(Event('key', decoded, self.flush_buf()))
|
||||
self.keymap = self.compiled_keymap
|
||||
25
Lib/_pyrepl/utils.py
vendored
Normal file
25
Lib/_pyrepl/utils.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import re
|
||||
import unicodedata
|
||||
import functools
|
||||
|
||||
ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")
|
||||
|
||||
|
||||
@functools.cache
|
||||
def str_width(c: str) -> int:
|
||||
if ord(c) < 128:
|
||||
return 1
|
||||
w = unicodedata.east_asian_width(c)
|
||||
if w in ('N', 'Na', 'H', 'A'):
|
||||
return 1
|
||||
return 2
|
||||
|
||||
|
||||
def wlen(s: str) -> int:
|
||||
if len(s) == 1 and s != '\x1a':
|
||||
return str_width(s)
|
||||
length = sum(str_width(i) for i in s)
|
||||
# remove lengths of any escape sequences
|
||||
sequence = ANSI_ESCAPE_SEQUENCE.findall(s)
|
||||
ctrl_z_cnt = s.count('\x1a')
|
||||
return length - sum(len(i) for i in sequence) + ctrl_z_cnt
|
||||
618
Lib/_pyrepl/windows_console.py
vendored
Normal file
618
Lib/_pyrepl/windows_console.py
vendored
Normal file
@@ -0,0 +1,618 @@
|
||||
# Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import msvcrt
|
||||
|
||||
from collections import deque
|
||||
import ctypes
|
||||
from ctypes.wintypes import (
|
||||
_COORD,
|
||||
WORD,
|
||||
SMALL_RECT,
|
||||
BOOL,
|
||||
HANDLE,
|
||||
CHAR,
|
||||
DWORD,
|
||||
WCHAR,
|
||||
SHORT,
|
||||
)
|
||||
from ctypes import Structure, POINTER, Union
|
||||
from .console import Event, Console
|
||||
from .trace import trace
|
||||
from .utils import wlen
|
||||
|
||||
try:
|
||||
from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined]
|
||||
except:
|
||||
# Keep MyPy happy off Windows
|
||||
from ctypes import CDLL as WinDLL, cdll as windll
|
||||
|
||||
def GetLastError() -> int:
|
||||
return 42
|
||||
|
||||
class WinError(OSError): # type: ignore[no-redef]
|
||||
def __init__(self, err: int | None, descr: str | None = None) -> None:
|
||||
self.err = err
|
||||
self.descr = descr
|
||||
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import IO
|
||||
|
||||
# Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
VK_MAP: dict[int, str] = {
|
||||
0x23: "end", # VK_END
|
||||
0x24: "home", # VK_HOME
|
||||
0x25: "left", # VK_LEFT
|
||||
0x26: "up", # VK_UP
|
||||
0x27: "right", # VK_RIGHT
|
||||
0x28: "down", # VK_DOWN
|
||||
0x2E: "delete", # VK_DELETE
|
||||
0x70: "f1", # VK_F1
|
||||
0x71: "f2", # VK_F2
|
||||
0x72: "f3", # VK_F3
|
||||
0x73: "f4", # VK_F4
|
||||
0x74: "f5", # VK_F5
|
||||
0x75: "f6", # VK_F6
|
||||
0x76: "f7", # VK_F7
|
||||
0x77: "f8", # VK_F8
|
||||
0x78: "f9", # VK_F9
|
||||
0x79: "f10", # VK_F10
|
||||
0x7A: "f11", # VK_F11
|
||||
0x7B: "f12", # VK_F12
|
||||
0x7C: "f13", # VK_F13
|
||||
0x7D: "f14", # VK_F14
|
||||
0x7E: "f15", # VK_F15
|
||||
0x7F: "f16", # VK_F16
|
||||
0x80: "f17", # VK_F17
|
||||
0x81: "f18", # VK_F18
|
||||
0x82: "f19", # VK_F19
|
||||
0x83: "f20", # VK_F20
|
||||
}
|
||||
|
||||
# Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
||||
ERASE_IN_LINE = "\x1b[K"
|
||||
MOVE_LEFT = "\x1b[{}D"
|
||||
MOVE_RIGHT = "\x1b[{}C"
|
||||
MOVE_UP = "\x1b[{}A"
|
||||
MOVE_DOWN = "\x1b[{}B"
|
||||
CLEAR = "\x1b[H\x1b[J"
|
||||
|
||||
|
||||
class _error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WindowsConsole(Console):
|
||||
def __init__(
|
||||
self,
|
||||
f_in: IO[bytes] | int = 0,
|
||||
f_out: IO[bytes] | int = 1,
|
||||
term: str = "",
|
||||
encoding: str = "",
|
||||
):
|
||||
super().__init__(f_in, f_out, term, encoding)
|
||||
|
||||
SetConsoleMode(
|
||||
OutHandle,
|
||||
ENABLE_WRAP_AT_EOL_OUTPUT
|
||||
| ENABLE_PROCESSED_OUTPUT
|
||||
| ENABLE_VIRTUAL_TERMINAL_PROCESSING,
|
||||
)
|
||||
self.screen: list[str] = []
|
||||
self.width = 80
|
||||
self.height = 25
|
||||
self.__offset = 0
|
||||
self.event_queue: deque[Event] = deque()
|
||||
try:
|
||||
self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined]
|
||||
except ValueError:
|
||||
# Console I/O is redirected, fallback...
|
||||
self.out = None
|
||||
|
||||
def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
|
||||
"""
|
||||
Refresh the console screen.
|
||||
|
||||
Parameters:
|
||||
- screen (list): List of strings representing the screen contents.
|
||||
- c_xy (tuple): Cursor position (x, y) on the screen.
|
||||
"""
|
||||
cx, cy = c_xy
|
||||
|
||||
while len(self.screen) < min(len(screen), self.height):
|
||||
self._hide_cursor()
|
||||
self._move_relative(0, len(self.screen) - 1)
|
||||
self.__write("\n")
|
||||
self.posxy = 0, len(self.screen)
|
||||
self.screen.append("")
|
||||
|
||||
px, py = self.posxy
|
||||
old_offset = offset = self.__offset
|
||||
height = self.height
|
||||
|
||||
# we make sure the cursor is on the screen, and that we're
|
||||
# using all of the screen if we can
|
||||
if cy < offset:
|
||||
offset = cy
|
||||
elif cy >= offset + height:
|
||||
offset = cy - height + 1
|
||||
scroll_lines = offset - old_offset
|
||||
|
||||
# Scrolling the buffer as the current input is greater than the visible
|
||||
# portion of the window. We need to scroll the visible portion and the
|
||||
# entire history
|
||||
self._scroll(scroll_lines, self._getscrollbacksize())
|
||||
self.posxy = self.posxy[0], self.posxy[1] + scroll_lines
|
||||
self.__offset += scroll_lines
|
||||
|
||||
for i in range(scroll_lines):
|
||||
self.screen.append("")
|
||||
elif offset > 0 and len(screen) < offset + height:
|
||||
offset = max(len(screen) - height, 0)
|
||||
screen.append("")
|
||||
|
||||
oldscr = self.screen[old_offset : old_offset + height]
|
||||
newscr = screen[offset : offset + height]
|
||||
|
||||
self.__offset = offset
|
||||
|
||||
self._hide_cursor()
|
||||
for (
|
||||
y,
|
||||
oldline,
|
||||
newline,
|
||||
) in zip(range(offset, offset + height), oldscr, newscr):
|
||||
if oldline != newline:
|
||||
self.__write_changed_line(y, oldline, newline, px)
|
||||
|
||||
y = len(newscr)
|
||||
while y < len(oldscr):
|
||||
self._move_relative(0, y)
|
||||
self.posxy = 0, y
|
||||
self._erase_to_end()
|
||||
y += 1
|
||||
|
||||
self._show_cursor()
|
||||
|
||||
self.screen = screen
|
||||
self.move_cursor(cx, cy)
|
||||
|
||||
@property
|
||||
def input_hook(self):
|
||||
try:
|
||||
import nt
|
||||
except ImportError:
|
||||
return None
|
||||
if nt._is_inputhook_installed():
|
||||
return nt._inputhook
|
||||
|
||||
def __write_changed_line(
|
||||
self, y: int, oldline: str, newline: str, px_coord: int
|
||||
) -> None:
|
||||
# this is frustrating; there's no reason to test (say)
|
||||
# self.dch1 inside the loop -- but alternative ways of
|
||||
# structuring this function are equally painful (I'm trying to
|
||||
# avoid writing code generators these days...)
|
||||
minlen = min(wlen(oldline), wlen(newline))
|
||||
x_pos = 0
|
||||
x_coord = 0
|
||||
|
||||
px_pos = 0
|
||||
j = 0
|
||||
for c in oldline:
|
||||
if j >= px_coord:
|
||||
break
|
||||
j += wlen(c)
|
||||
px_pos += 1
|
||||
|
||||
# reuse the oldline as much as possible, but stop as soon as we
|
||||
# encounter an ESCAPE, because it might be the start of an escape
|
||||
# sequene
|
||||
while (
|
||||
x_coord < minlen
|
||||
and oldline[x_pos] == newline[x_pos]
|
||||
and newline[x_pos] != "\x1b"
|
||||
):
|
||||
x_coord += wlen(newline[x_pos])
|
||||
x_pos += 1
|
||||
|
||||
self._hide_cursor()
|
||||
self._move_relative(x_coord, y)
|
||||
if wlen(oldline) > wlen(newline):
|
||||
self._erase_to_end()
|
||||
|
||||
self.__write(newline[x_pos:])
|
||||
if wlen(newline) == self.width:
|
||||
# If we wrapped we want to start at the next line
|
||||
self._move_relative(0, y + 1)
|
||||
self.posxy = 0, y + 1
|
||||
else:
|
||||
self.posxy = wlen(newline), y
|
||||
|
||||
if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline:
|
||||
# ANSI escape characters are present, so we can't assume
|
||||
# anything about the position of the cursor. Moving the cursor
|
||||
# to the left margin should work to get to a known position.
|
||||
self.move_cursor(0, y)
|
||||
|
||||
def _scroll(
|
||||
self, top: int, bottom: int, left: int | None = None, right: int | None = None
|
||||
) -> None:
|
||||
scroll_rect = SMALL_RECT()
|
||||
scroll_rect.Top = SHORT(top)
|
||||
scroll_rect.Bottom = SHORT(bottom)
|
||||
scroll_rect.Left = SHORT(0 if left is None else left)
|
||||
scroll_rect.Right = SHORT(
|
||||
self.getheightwidth()[1] - 1 if right is None else right
|
||||
)
|
||||
destination_origin = _COORD()
|
||||
fill_info = CHAR_INFO()
|
||||
fill_info.UnicodeChar = " "
|
||||
|
||||
if not ScrollConsoleScreenBuffer(
|
||||
OutHandle, scroll_rect, None, destination_origin, fill_info
|
||||
):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
def _hide_cursor(self):
|
||||
self.__write("\x1b[?25l")
|
||||
|
||||
def _show_cursor(self):
|
||||
self.__write("\x1b[?25h")
|
||||
|
||||
def _enable_blinking(self):
|
||||
self.__write("\x1b[?12h")
|
||||
|
||||
def _disable_blinking(self):
|
||||
self.__write("\x1b[?12l")
|
||||
|
||||
def __write(self, text: str) -> None:
|
||||
if "\x1a" in text:
|
||||
text = ''.join(["^Z" if x == '\x1a' else x for x in text])
|
||||
|
||||
if self.out is not None:
|
||||
self.out.write(text.encode(self.encoding, "replace"))
|
||||
self.out.flush()
|
||||
else:
|
||||
os.write(self.output_fd, text.encode(self.encoding, "replace"))
|
||||
|
||||
@property
|
||||
def screen_xy(self) -> tuple[int, int]:
|
||||
info = CONSOLE_SCREEN_BUFFER_INFO()
|
||||
if not GetConsoleScreenBufferInfo(OutHandle, info):
|
||||
raise WinError(GetLastError())
|
||||
return info.dwCursorPosition.X, info.dwCursorPosition.Y
|
||||
|
||||
def _erase_to_end(self) -> None:
|
||||
self.__write(ERASE_IN_LINE)
|
||||
|
||||
def prepare(self) -> None:
|
||||
trace("prepare")
|
||||
self.screen = []
|
||||
self.height, self.width = self.getheightwidth()
|
||||
|
||||
self.posxy = 0, 0
|
||||
self.__gone_tall = 0
|
||||
self.__offset = 0
|
||||
|
||||
def restore(self) -> None:
|
||||
pass
|
||||
|
||||
def _move_relative(self, x: int, y: int) -> None:
|
||||
"""Moves relative to the current posxy"""
|
||||
dx = x - self.posxy[0]
|
||||
dy = y - self.posxy[1]
|
||||
if dx < 0:
|
||||
self.__write(MOVE_LEFT.format(-dx))
|
||||
elif dx > 0:
|
||||
self.__write(MOVE_RIGHT.format(dx))
|
||||
|
||||
if dy < 0:
|
||||
self.__write(MOVE_UP.format(-dy))
|
||||
elif dy > 0:
|
||||
self.__write(MOVE_DOWN.format(dy))
|
||||
|
||||
def move_cursor(self, x: int, y: int) -> None:
|
||||
if x < 0 or y < 0:
|
||||
raise ValueError(f"Bad cursor position {x}, {y}")
|
||||
|
||||
if y < self.__offset or y >= self.__offset + self.height:
|
||||
self.event_queue.insert(0, Event("scroll", ""))
|
||||
else:
|
||||
self._move_relative(x, y)
|
||||
self.posxy = x, y
|
||||
|
||||
def set_cursor_vis(self, visible: bool) -> None:
|
||||
if visible:
|
||||
self._show_cursor()
|
||||
else:
|
||||
self._hide_cursor()
|
||||
|
||||
def getheightwidth(self) -> tuple[int, int]:
|
||||
"""Return (height, width) where height and width are the height
|
||||
and width of the terminal window in characters."""
|
||||
info = CONSOLE_SCREEN_BUFFER_INFO()
|
||||
if not GetConsoleScreenBufferInfo(OutHandle, info):
|
||||
raise WinError(GetLastError())
|
||||
return (
|
||||
info.srWindow.Bottom - info.srWindow.Top + 1,
|
||||
info.srWindow.Right - info.srWindow.Left + 1,
|
||||
)
|
||||
|
||||
def _getscrollbacksize(self) -> int:
|
||||
info = CONSOLE_SCREEN_BUFFER_INFO()
|
||||
if not GetConsoleScreenBufferInfo(OutHandle, info):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
return info.srWindow.Bottom # type: ignore[no-any-return]
|
||||
|
||||
def _read_input(self, block: bool = True) -> INPUT_RECORD | None:
|
||||
if not block:
|
||||
events = DWORD()
|
||||
if not GetNumberOfConsoleInputEvents(InHandle, events):
|
||||
raise WinError(GetLastError())
|
||||
if not events.value:
|
||||
return None
|
||||
|
||||
rec = INPUT_RECORD()
|
||||
read = DWORD()
|
||||
if not ReadConsoleInput(InHandle, rec, 1, read):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
return rec
|
||||
|
||||
def get_event(self, block: bool = True) -> Event | None:
|
||||
"""Return an Event instance. Returns None if |block| is false
|
||||
and there is no event pending, otherwise waits for the
|
||||
completion of an event."""
|
||||
if self.event_queue:
|
||||
return self.event_queue.pop()
|
||||
|
||||
while True:
|
||||
rec = self._read_input(block)
|
||||
if rec is None:
|
||||
return None
|
||||
|
||||
if rec.EventType == WINDOW_BUFFER_SIZE_EVENT:
|
||||
return Event("resize", "")
|
||||
|
||||
if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown:
|
||||
# Only process keys and keydown events
|
||||
if block:
|
||||
continue
|
||||
return None
|
||||
|
||||
key = rec.Event.KeyEvent.uChar.UnicodeChar
|
||||
|
||||
if rec.Event.KeyEvent.uChar.UnicodeChar == "\r":
|
||||
# Make enter make unix-like
|
||||
return Event(evt="key", data="\n", raw=b"\n")
|
||||
elif rec.Event.KeyEvent.wVirtualKeyCode == 8:
|
||||
# Turn backspace directly into the command
|
||||
return Event(
|
||||
evt="key",
|
||||
data="backspace",
|
||||
raw=rec.Event.KeyEvent.uChar.UnicodeChar,
|
||||
)
|
||||
elif rec.Event.KeyEvent.uChar.UnicodeChar == "\x00":
|
||||
# Handle special keys like arrow keys and translate them into the appropriate command
|
||||
code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode)
|
||||
if code:
|
||||
return Event(
|
||||
evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar
|
||||
)
|
||||
if block:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar)
|
||||
|
||||
def push_char(self, char: int | bytes) -> None:
|
||||
"""
|
||||
Push a character to the console event queue.
|
||||
"""
|
||||
raise NotImplementedError("push_char not supported on Windows")
|
||||
|
||||
def beep(self) -> None:
|
||||
self.__write("\x07")
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Wipe the screen"""
|
||||
self.__write(CLEAR)
|
||||
self.posxy = 0, 0
|
||||
self.screen = [""]
|
||||
|
||||
def finish(self) -> None:
|
||||
"""Move the cursor to the end of the display and otherwise get
|
||||
ready for end. XXX could be merged with restore? Hmm."""
|
||||
y = len(self.screen) - 1
|
||||
while y >= 0 and not self.screen[y]:
|
||||
y -= 1
|
||||
self._move_relative(0, min(y, self.height + self.__offset - 1))
|
||||
self.__write("\r\n")
|
||||
|
||||
def flushoutput(self) -> None:
|
||||
"""Flush all output to the screen (assuming there's some
|
||||
buffering going on somewhere).
|
||||
|
||||
All output on Windows is unbuffered so this is a nop"""
|
||||
pass
|
||||
|
||||
def forgetinput(self) -> None:
|
||||
"""Forget all pending, but not yet processed input."""
|
||||
if not FlushConsoleInputBuffer(InHandle):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
def getpending(self) -> Event:
|
||||
"""Return the characters that have been typed but not yet
|
||||
processed."""
|
||||
return Event("key", "", b"")
|
||||
|
||||
def wait(self, timeout: float | None) -> bool:
|
||||
"""Wait for an event."""
|
||||
# Poor man's Windows select loop
|
||||
start_time = time.time()
|
||||
while True:
|
||||
if msvcrt.kbhit(): # type: ignore[attr-defined]
|
||||
return True
|
||||
if timeout and time.time() - start_time > timeout / 1000:
|
||||
return False
|
||||
time.sleep(0.01)
|
||||
|
||||
def repaint(self) -> None:
|
||||
raise NotImplementedError("No repaint support")
|
||||
|
||||
|
||||
# Windows interop
|
||||
class CONSOLE_SCREEN_BUFFER_INFO(Structure):
|
||||
_fields_ = [
|
||||
("dwSize", _COORD),
|
||||
("dwCursorPosition", _COORD),
|
||||
("wAttributes", WORD),
|
||||
("srWindow", SMALL_RECT),
|
||||
("dwMaximumWindowSize", _COORD),
|
||||
]
|
||||
|
||||
|
||||
class CONSOLE_CURSOR_INFO(Structure):
|
||||
_fields_ = [
|
||||
("dwSize", DWORD),
|
||||
("bVisible", BOOL),
|
||||
]
|
||||
|
||||
|
||||
class CHAR_INFO(Structure):
|
||||
_fields_ = [
|
||||
("UnicodeChar", WCHAR),
|
||||
("Attributes", WORD),
|
||||
]
|
||||
|
||||
|
||||
class Char(Union):
|
||||
_fields_ = [
|
||||
("UnicodeChar", WCHAR),
|
||||
("Char", CHAR),
|
||||
]
|
||||
|
||||
|
||||
class KeyEvent(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("bKeyDown", BOOL),
|
||||
("wRepeatCount", WORD),
|
||||
("wVirtualKeyCode", WORD),
|
||||
("wVirtualScanCode", WORD),
|
||||
("uChar", Char),
|
||||
("dwControlKeyState", DWORD),
|
||||
]
|
||||
|
||||
|
||||
class WindowsBufferSizeEvent(ctypes.Structure):
|
||||
_fields_ = [("dwSize", _COORD)]
|
||||
|
||||
|
||||
class ConsoleEvent(ctypes.Union):
|
||||
_fields_ = [
|
||||
("KeyEvent", KeyEvent),
|
||||
("WindowsBufferSizeEvent", WindowsBufferSizeEvent),
|
||||
]
|
||||
|
||||
|
||||
class INPUT_RECORD(Structure):
|
||||
_fields_ = [("EventType", WORD), ("Event", ConsoleEvent)]
|
||||
|
||||
|
||||
KEY_EVENT = 0x01
|
||||
FOCUS_EVENT = 0x10
|
||||
MENU_EVENT = 0x08
|
||||
MOUSE_EVENT = 0x02
|
||||
WINDOW_BUFFER_SIZE_EVENT = 0x04
|
||||
|
||||
ENABLE_PROCESSED_OUTPUT = 0x01
|
||||
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
|
||||
|
||||
STD_INPUT_HANDLE = -10
|
||||
STD_OUTPUT_HANDLE = -11
|
||||
|
||||
if sys.platform == "win32":
|
||||
_KERNEL32 = WinDLL("kernel32", use_last_error=True)
|
||||
|
||||
GetStdHandle = windll.kernel32.GetStdHandle
|
||||
GetStdHandle.argtypes = [DWORD]
|
||||
GetStdHandle.restype = HANDLE
|
||||
|
||||
GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo
|
||||
GetConsoleScreenBufferInfo.argtypes = [
|
||||
HANDLE,
|
||||
ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
|
||||
]
|
||||
GetConsoleScreenBufferInfo.restype = BOOL
|
||||
|
||||
ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW
|
||||
ScrollConsoleScreenBuffer.argtypes = [
|
||||
HANDLE,
|
||||
POINTER(SMALL_RECT),
|
||||
POINTER(SMALL_RECT),
|
||||
_COORD,
|
||||
POINTER(CHAR_INFO),
|
||||
]
|
||||
ScrollConsoleScreenBuffer.restype = BOOL
|
||||
|
||||
SetConsoleMode = _KERNEL32.SetConsoleMode
|
||||
SetConsoleMode.argtypes = [HANDLE, DWORD]
|
||||
SetConsoleMode.restype = BOOL
|
||||
|
||||
ReadConsoleInput = _KERNEL32.ReadConsoleInputW
|
||||
ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)]
|
||||
ReadConsoleInput.restype = BOOL
|
||||
|
||||
GetNumberOfConsoleInputEvents = _KERNEL32.GetNumberOfConsoleInputEvents
|
||||
GetNumberOfConsoleInputEvents.argtypes = [HANDLE, POINTER(DWORD)]
|
||||
GetNumberOfConsoleInputEvents.restype = BOOL
|
||||
|
||||
FlushConsoleInputBuffer = _KERNEL32.FlushConsoleInputBuffer
|
||||
FlushConsoleInputBuffer.argtypes = [HANDLE]
|
||||
FlushConsoleInputBuffer.restype = BOOL
|
||||
|
||||
OutHandle = GetStdHandle(STD_OUTPUT_HANDLE)
|
||||
InHandle = GetStdHandle(STD_INPUT_HANDLE)
|
||||
else:
|
||||
|
||||
def _win_only(*args, **kwargs):
|
||||
raise NotImplementedError("Windows only")
|
||||
|
||||
GetStdHandle = _win_only
|
||||
GetConsoleScreenBufferInfo = _win_only
|
||||
ScrollConsoleScreenBuffer = _win_only
|
||||
SetConsoleMode = _win_only
|
||||
ReadConsoleInput = _win_only
|
||||
GetNumberOfConsoleInputEvents = _win_only
|
||||
FlushConsoleInputBuffer = _win_only
|
||||
OutHandle = 0
|
||||
InHandle = 0
|
||||
3
Lib/_sitebuiltins.py
vendored
3
Lib/_sitebuiltins.py
vendored
@@ -10,7 +10,6 @@ The objects used by the site module to add custom builtins.
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class Quitter(object):
|
||||
def __init__(self, name, eof):
|
||||
self.name = name
|
||||
@@ -48,7 +47,7 @@ class _Printer(object):
|
||||
data = None
|
||||
for filename in self.__filenames:
|
||||
try:
|
||||
with open(filename, "r") as fp:
|
||||
with open(filename, encoding='utf-8') as fp:
|
||||
data = fp.read()
|
||||
break
|
||||
except OSError:
|
||||
|
||||
565
Lib/_strptime.py
vendored
Normal file
565
Lib/_strptime.py
vendored
Normal file
@@ -0,0 +1,565 @@
|
||||
"""Strptime-related classes and functions.
|
||||
|
||||
CLASSES:
|
||||
LocaleTime -- Discovers and stores locale-specific time information
|
||||
TimeRE -- Creates regexes for pattern matching a string of text containing
|
||||
time information
|
||||
|
||||
FUNCTIONS:
|
||||
_getlang -- Figure out what language is being used for the locale
|
||||
strptime -- Calculates the time struct represented by the passed-in string
|
||||
|
||||
"""
|
||||
import time
|
||||
import locale
|
||||
import calendar
|
||||
from re import compile as re_compile
|
||||
from re import IGNORECASE
|
||||
from re import escape as re_escape
|
||||
from datetime import (date as datetime_date,
|
||||
timedelta as datetime_timedelta,
|
||||
timezone as datetime_timezone)
|
||||
from _thread import allocate_lock as _thread_allocate_lock
|
||||
|
||||
__all__ = []
|
||||
|
||||
def _getlang():
|
||||
# Figure out what the current language is set to.
|
||||
return locale.getlocale(locale.LC_TIME)
|
||||
|
||||
class LocaleTime(object):
|
||||
"""Stores and handles locale-specific information related to time.
|
||||
|
||||
ATTRIBUTES:
|
||||
f_weekday -- full weekday names (7-item list)
|
||||
a_weekday -- abbreviated weekday names (7-item list)
|
||||
f_month -- full month names (13-item list; dummy value in [0], which
|
||||
is added by code)
|
||||
a_month -- abbreviated month names (13-item list, dummy value in
|
||||
[0], which is added by code)
|
||||
am_pm -- AM/PM representation (2-item list)
|
||||
LC_date_time -- format string for date/time representation (string)
|
||||
LC_date -- format string for date representation (string)
|
||||
LC_time -- format string for time representation (string)
|
||||
timezone -- daylight- and non-daylight-savings timezone representation
|
||||
(2-item list of sets)
|
||||
lang -- Language used by instance (2-item tuple)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Set all attributes.
|
||||
|
||||
Order of methods called matters for dependency reasons.
|
||||
|
||||
The locale language is set at the offset and then checked again before
|
||||
exiting. This is to make sure that the attributes were not set with a
|
||||
mix of information from more than one locale. This would most likely
|
||||
happen when using threads where one thread calls a locale-dependent
|
||||
function while another thread changes the locale while the function in
|
||||
the other thread is still running. Proper coding would call for
|
||||
locks to prevent changing the locale while locale-dependent code is
|
||||
running. The check here is done in case someone does not think about
|
||||
doing this.
|
||||
|
||||
Only other possible issue is if someone changed the timezone and did
|
||||
not call tz.tzset . That is an issue for the programmer, though,
|
||||
since changing the timezone is worthless without that call.
|
||||
|
||||
"""
|
||||
self.lang = _getlang()
|
||||
self.__calc_weekday()
|
||||
self.__calc_month()
|
||||
self.__calc_am_pm()
|
||||
self.__calc_timezone()
|
||||
self.__calc_date_time()
|
||||
if _getlang() != self.lang:
|
||||
raise ValueError("locale changed during initialization")
|
||||
if time.tzname != self.tzname or time.daylight != self.daylight:
|
||||
raise ValueError("timezone changed during initialization")
|
||||
|
||||
def __calc_weekday(self):
|
||||
# Set self.a_weekday and self.f_weekday using the calendar
|
||||
# module.
|
||||
a_weekday = [calendar.day_abbr[i].lower() for i in range(7)]
|
||||
f_weekday = [calendar.day_name[i].lower() for i in range(7)]
|
||||
self.a_weekday = a_weekday
|
||||
self.f_weekday = f_weekday
|
||||
|
||||
def __calc_month(self):
|
||||
# Set self.f_month and self.a_month using the calendar module.
|
||||
a_month = [calendar.month_abbr[i].lower() for i in range(13)]
|
||||
f_month = [calendar.month_name[i].lower() for i in range(13)]
|
||||
self.a_month = a_month
|
||||
self.f_month = f_month
|
||||
|
||||
def __calc_am_pm(self):
|
||||
# Set self.am_pm by using time.strftime().
|
||||
|
||||
# The magic date (1999,3,17,hour,44,55,2,76,0) is not really that
|
||||
# magical; just happened to have used it everywhere else where a
|
||||
# static date was needed.
|
||||
am_pm = []
|
||||
for hour in (1, 22):
|
||||
time_tuple = time.struct_time((1999,3,17,hour,44,55,2,76,0))
|
||||
am_pm.append(time.strftime("%p", time_tuple).lower())
|
||||
self.am_pm = am_pm
|
||||
|
||||
def __calc_date_time(self):
|
||||
# Set self.date_time, self.date, & self.time by using
|
||||
# time.strftime().
|
||||
|
||||
# Use (1999,3,17,22,44,55,2,76,0) for magic date because the amount of
|
||||
# overloaded numbers is minimized. The order in which searches for
|
||||
# values within the format string is very important; it eliminates
|
||||
# possible ambiguity for what something represents.
|
||||
time_tuple = time.struct_time((1999,3,17,22,44,55,2,76,0))
|
||||
date_time = [None, None, None]
|
||||
date_time[0] = time.strftime("%c", time_tuple).lower()
|
||||
date_time[1] = time.strftime("%x", time_tuple).lower()
|
||||
date_time[2] = time.strftime("%X", time_tuple).lower()
|
||||
replacement_pairs = [('%', '%%'), (self.f_weekday[2], '%A'),
|
||||
(self.f_month[3], '%B'), (self.a_weekday[2], '%a'),
|
||||
(self.a_month[3], '%b'), (self.am_pm[1], '%p'),
|
||||
('1999', '%Y'), ('99', '%y'), ('22', '%H'),
|
||||
('44', '%M'), ('55', '%S'), ('76', '%j'),
|
||||
('17', '%d'), ('03', '%m'), ('3', '%m'),
|
||||
# '3' needed for when no leading zero.
|
||||
('2', '%w'), ('10', '%I')]
|
||||
replacement_pairs.extend([(tz, "%Z") for tz_values in self.timezone
|
||||
for tz in tz_values])
|
||||
for offset,directive in ((0,'%c'), (1,'%x'), (2,'%X')):
|
||||
current_format = date_time[offset]
|
||||
for old, new in replacement_pairs:
|
||||
# Must deal with possible lack of locale info
|
||||
# manifesting itself as the empty string (e.g., Swedish's
|
||||
# lack of AM/PM info) or a platform returning a tuple of empty
|
||||
# strings (e.g., MacOS 9 having timezone as ('','')).
|
||||
if old:
|
||||
current_format = current_format.replace(old, new)
|
||||
# If %W is used, then Sunday, 2005-01-03 will fall on week 0 since
|
||||
# 2005-01-03 occurs before the first Monday of the year. Otherwise
|
||||
# %U is used.
|
||||
time_tuple = time.struct_time((1999,1,3,1,1,1,6,3,0))
|
||||
if '00' in time.strftime(directive, time_tuple):
|
||||
U_W = '%W'
|
||||
else:
|
||||
U_W = '%U'
|
||||
date_time[offset] = current_format.replace('11', U_W)
|
||||
self.LC_date_time = date_time[0]
|
||||
self.LC_date = date_time[1]
|
||||
self.LC_time = date_time[2]
|
||||
|
||||
def __calc_timezone(self):
|
||||
# Set self.timezone by using time.tzname.
|
||||
# Do not worry about possibility of time.tzname[0] == time.tzname[1]
|
||||
# and time.daylight; handle that in strptime.
|
||||
try:
|
||||
time.tzset()
|
||||
except AttributeError:
|
||||
pass
|
||||
self.tzname = time.tzname
|
||||
self.daylight = time.daylight
|
||||
no_saving = frozenset({"utc", "gmt", self.tzname[0].lower()})
|
||||
if self.daylight:
|
||||
has_saving = frozenset({self.tzname[1].lower()})
|
||||
else:
|
||||
has_saving = frozenset()
|
||||
self.timezone = (no_saving, has_saving)
|
||||
|
||||
|
||||
class TimeRE(dict):
|
||||
"""Handle conversion from format directives to regexes."""
|
||||
|
||||
def __init__(self, locale_time=None):
|
||||
"""Create keys/values.
|
||||
|
||||
Order of execution is important for dependency reasons.
|
||||
|
||||
"""
|
||||
if locale_time:
|
||||
self.locale_time = locale_time
|
||||
else:
|
||||
self.locale_time = LocaleTime()
|
||||
base = super()
|
||||
base.__init__({
|
||||
# The " [1-9]" part of the regex is to make %c from ANSI C work
|
||||
'd': r"(?P<d>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
|
||||
'f': r"(?P<f>[0-9]{1,6})",
|
||||
'H': r"(?P<H>2[0-3]|[0-1]\d|\d)",
|
||||
'I': r"(?P<I>1[0-2]|0[1-9]|[1-9])",
|
||||
'G': r"(?P<G>\d\d\d\d)",
|
||||
'j': r"(?P<j>36[0-6]|3[0-5]\d|[1-2]\d\d|0[1-9]\d|00[1-9]|[1-9]\d|0[1-9]|[1-9])",
|
||||
'm': r"(?P<m>1[0-2]|0[1-9]|[1-9])",
|
||||
'M': r"(?P<M>[0-5]\d|\d)",
|
||||
'S': r"(?P<S>6[0-1]|[0-5]\d|\d)",
|
||||
'U': r"(?P<U>5[0-3]|[0-4]\d|\d)",
|
||||
'w': r"(?P<w>[0-6])",
|
||||
'u': r"(?P<u>[1-7])",
|
||||
'V': r"(?P<V>5[0-3]|0[1-9]|[1-4]\d|\d)",
|
||||
# W is set below by using 'U'
|
||||
'y': r"(?P<y>\d\d)",
|
||||
#XXX: Does 'Y' need to worry about having less or more than
|
||||
# 4 digits?
|
||||
'Y': r"(?P<Y>\d\d\d\d)",
|
||||
'z': r"(?P<z>[+-]\d\d:?[0-5]\d(:?[0-5]\d(\.\d{1,6})?)?|(?-i:Z))",
|
||||
'A': self.__seqToRE(self.locale_time.f_weekday, 'A'),
|
||||
'a': self.__seqToRE(self.locale_time.a_weekday, 'a'),
|
||||
'B': self.__seqToRE(self.locale_time.f_month[1:], 'B'),
|
||||
'b': self.__seqToRE(self.locale_time.a_month[1:], 'b'),
|
||||
'p': self.__seqToRE(self.locale_time.am_pm, 'p'),
|
||||
'Z': self.__seqToRE((tz for tz_names in self.locale_time.timezone
|
||||
for tz in tz_names),
|
||||
'Z'),
|
||||
'%': '%'})
|
||||
base.__setitem__('W', base.__getitem__('U').replace('U', 'W'))
|
||||
base.__setitem__('c', self.pattern(self.locale_time.LC_date_time))
|
||||
base.__setitem__('x', self.pattern(self.locale_time.LC_date))
|
||||
base.__setitem__('X', self.pattern(self.locale_time.LC_time))
|
||||
|
||||
def __seqToRE(self, to_convert, directive):
|
||||
"""Convert a list to a regex string for matching a directive.
|
||||
|
||||
Want possible matching values to be from longest to shortest. This
|
||||
prevents the possibility of a match occurring for a value that also
|
||||
a substring of a larger value that should have matched (e.g., 'abc'
|
||||
matching when 'abcdef' should have been the match).
|
||||
|
||||
"""
|
||||
to_convert = sorted(to_convert, key=len, reverse=True)
|
||||
for value in to_convert:
|
||||
if value != '':
|
||||
break
|
||||
else:
|
||||
return ''
|
||||
regex = '|'.join(re_escape(stuff) for stuff in to_convert)
|
||||
regex = '(?P<%s>%s' % (directive, regex)
|
||||
return '%s)' % regex
|
||||
|
||||
def pattern(self, format):
|
||||
"""Return regex pattern for the format string.
|
||||
|
||||
Need to make sure that any characters that might be interpreted as
|
||||
regex syntax are escaped.
|
||||
|
||||
"""
|
||||
processed_format = ''
|
||||
# The sub() call escapes all characters that might be misconstrued
|
||||
# as regex syntax. Cannot use re.escape since we have to deal with
|
||||
# format directives (%m, etc.).
|
||||
regex_chars = re_compile(r"([\\.^$*+?\(\){}\[\]|])")
|
||||
format = regex_chars.sub(r"\\\1", format)
|
||||
whitespace_replacement = re_compile(r'\s+')
|
||||
format = whitespace_replacement.sub(r'\\s+', format)
|
||||
while '%' in format:
|
||||
directive_index = format.index('%')+1
|
||||
processed_format = "%s%s%s" % (processed_format,
|
||||
format[:directive_index-1],
|
||||
self[format[directive_index]])
|
||||
format = format[directive_index+1:]
|
||||
return "%s%s" % (processed_format, format)
|
||||
|
||||
def compile(self, format):
|
||||
"""Return a compiled re object for the format string."""
|
||||
return re_compile(self.pattern(format), IGNORECASE)
|
||||
|
||||
_cache_lock = _thread_allocate_lock()
|
||||
# DO NOT modify _TimeRE_cache or _regex_cache without acquiring the cache lock
|
||||
# first!
|
||||
_TimeRE_cache = TimeRE()
|
||||
_CACHE_MAX_SIZE = 5 # Max number of regexes stored in _regex_cache
|
||||
_regex_cache = {}
|
||||
|
||||
def _calc_julian_from_U_or_W(year, week_of_year, day_of_week, week_starts_Mon):
|
||||
"""Calculate the Julian day based on the year, week of the year, and day of
|
||||
the week, with week_start_day representing whether the week of the year
|
||||
assumes the week starts on Sunday or Monday (6 or 0)."""
|
||||
first_weekday = datetime_date(year, 1, 1).weekday()
|
||||
# If we are dealing with the %U directive (week starts on Sunday), it's
|
||||
# easier to just shift the view to Sunday being the first day of the
|
||||
# week.
|
||||
if not week_starts_Mon:
|
||||
first_weekday = (first_weekday + 1) % 7
|
||||
day_of_week = (day_of_week + 1) % 7
|
||||
# Need to watch out for a week 0 (when the first day of the year is not
|
||||
# the same as that specified by %U or %W).
|
||||
week_0_length = (7 - first_weekday) % 7
|
||||
if week_of_year == 0:
|
||||
return 1 + day_of_week - first_weekday
|
||||
else:
|
||||
days_to_week = week_0_length + (7 * (week_of_year - 1))
|
||||
return 1 + days_to_week + day_of_week
|
||||
|
||||
|
||||
def _strptime(data_string, format="%a %b %d %H:%M:%S %Y"):
|
||||
"""Return a 2-tuple consisting of a time struct and an int containing
|
||||
the number of microseconds based on the input string and the
|
||||
format string."""
|
||||
|
||||
for index, arg in enumerate([data_string, format]):
|
||||
if not isinstance(arg, str):
|
||||
msg = "strptime() argument {} must be str, not {}"
|
||||
raise TypeError(msg.format(index, type(arg)))
|
||||
|
||||
global _TimeRE_cache, _regex_cache
|
||||
with _cache_lock:
|
||||
locale_time = _TimeRE_cache.locale_time
|
||||
if (_getlang() != locale_time.lang or
|
||||
time.tzname != locale_time.tzname or
|
||||
time.daylight != locale_time.daylight):
|
||||
_TimeRE_cache = TimeRE()
|
||||
_regex_cache.clear()
|
||||
locale_time = _TimeRE_cache.locale_time
|
||||
if len(_regex_cache) > _CACHE_MAX_SIZE:
|
||||
_regex_cache.clear()
|
||||
format_regex = _regex_cache.get(format)
|
||||
if not format_regex:
|
||||
try:
|
||||
format_regex = _TimeRE_cache.compile(format)
|
||||
# KeyError raised when a bad format is found; can be specified as
|
||||
# \\, in which case it was a stray % but with a space after it
|
||||
except KeyError as err:
|
||||
bad_directive = err.args[0]
|
||||
if bad_directive == "\\":
|
||||
bad_directive = "%"
|
||||
del err
|
||||
raise ValueError("'%s' is a bad directive in format '%s'" %
|
||||
(bad_directive, format)) from None
|
||||
# IndexError only occurs when the format string is "%"
|
||||
except IndexError:
|
||||
raise ValueError("stray %% in format '%s'" % format) from None
|
||||
_regex_cache[format] = format_regex
|
||||
found = format_regex.match(data_string)
|
||||
if not found:
|
||||
raise ValueError("time data %r does not match format %r" %
|
||||
(data_string, format))
|
||||
if len(data_string) != found.end():
|
||||
raise ValueError("unconverted data remains: %s" %
|
||||
data_string[found.end():])
|
||||
|
||||
iso_year = year = None
|
||||
month = day = 1
|
||||
hour = minute = second = fraction = 0
|
||||
tz = -1
|
||||
gmtoff = None
|
||||
gmtoff_fraction = 0
|
||||
iso_week = week_of_year = None
|
||||
week_of_year_start = None
|
||||
# weekday and julian defaulted to None so as to signal need to calculate
|
||||
# values
|
||||
weekday = julian = None
|
||||
found_dict = found.groupdict()
|
||||
for group_key in found_dict.keys():
|
||||
# Directives not explicitly handled below:
|
||||
# c, x, X
|
||||
# handled by making out of other directives
|
||||
# U, W
|
||||
# worthless without day of the week
|
||||
if group_key == 'y':
|
||||
year = int(found_dict['y'])
|
||||
# Open Group specification for strptime() states that a %y
|
||||
#value in the range of [00, 68] is in the century 2000, while
|
||||
#[69,99] is in the century 1900
|
||||
if year <= 68:
|
||||
year += 2000
|
||||
else:
|
||||
year += 1900
|
||||
elif group_key == 'Y':
|
||||
year = int(found_dict['Y'])
|
||||
elif group_key == 'G':
|
||||
iso_year = int(found_dict['G'])
|
||||
elif group_key == 'm':
|
||||
month = int(found_dict['m'])
|
||||
elif group_key == 'B':
|
||||
month = locale_time.f_month.index(found_dict['B'].lower())
|
||||
elif group_key == 'b':
|
||||
month = locale_time.a_month.index(found_dict['b'].lower())
|
||||
elif group_key == 'd':
|
||||
day = int(found_dict['d'])
|
||||
elif group_key == 'H':
|
||||
hour = int(found_dict['H'])
|
||||
elif group_key == 'I':
|
||||
hour = int(found_dict['I'])
|
||||
ampm = found_dict.get('p', '').lower()
|
||||
# If there was no AM/PM indicator, we'll treat this like AM
|
||||
if ampm in ('', locale_time.am_pm[0]):
|
||||
# We're in AM so the hour is correct unless we're
|
||||
# looking at 12 midnight.
|
||||
# 12 midnight == 12 AM == hour 0
|
||||
if hour == 12:
|
||||
hour = 0
|
||||
elif ampm == locale_time.am_pm[1]:
|
||||
# We're in PM so we need to add 12 to the hour unless
|
||||
# we're looking at 12 noon.
|
||||
# 12 noon == 12 PM == hour 12
|
||||
if hour != 12:
|
||||
hour += 12
|
||||
elif group_key == 'M':
|
||||
minute = int(found_dict['M'])
|
||||
elif group_key == 'S':
|
||||
second = int(found_dict['S'])
|
||||
elif group_key == 'f':
|
||||
s = found_dict['f']
|
||||
# Pad to always return microseconds.
|
||||
s += "0" * (6 - len(s))
|
||||
fraction = int(s)
|
||||
elif group_key == 'A':
|
||||
weekday = locale_time.f_weekday.index(found_dict['A'].lower())
|
||||
elif group_key == 'a':
|
||||
weekday = locale_time.a_weekday.index(found_dict['a'].lower())
|
||||
elif group_key == 'w':
|
||||
weekday = int(found_dict['w'])
|
||||
if weekday == 0:
|
||||
weekday = 6
|
||||
else:
|
||||
weekday -= 1
|
||||
elif group_key == 'u':
|
||||
weekday = int(found_dict['u'])
|
||||
weekday -= 1
|
||||
elif group_key == 'j':
|
||||
julian = int(found_dict['j'])
|
||||
elif group_key in ('U', 'W'):
|
||||
week_of_year = int(found_dict[group_key])
|
||||
if group_key == 'U':
|
||||
# U starts week on Sunday.
|
||||
week_of_year_start = 6
|
||||
else:
|
||||
# W starts week on Monday.
|
||||
week_of_year_start = 0
|
||||
elif group_key == 'V':
|
||||
iso_week = int(found_dict['V'])
|
||||
elif group_key == 'z':
|
||||
z = found_dict['z']
|
||||
if z == 'Z':
|
||||
gmtoff = 0
|
||||
else:
|
||||
if z[3] == ':':
|
||||
z = z[:3] + z[4:]
|
||||
if len(z) > 5:
|
||||
if z[5] != ':':
|
||||
msg = f"Inconsistent use of : in {found_dict['z']}"
|
||||
raise ValueError(msg)
|
||||
z = z[:5] + z[6:]
|
||||
hours = int(z[1:3])
|
||||
minutes = int(z[3:5])
|
||||
seconds = int(z[5:7] or 0)
|
||||
gmtoff = (hours * 60 * 60) + (minutes * 60) + seconds
|
||||
gmtoff_remainder = z[8:]
|
||||
# Pad to always return microseconds.
|
||||
gmtoff_remainder_padding = "0" * (6 - len(gmtoff_remainder))
|
||||
gmtoff_fraction = int(gmtoff_remainder + gmtoff_remainder_padding)
|
||||
if z.startswith("-"):
|
||||
gmtoff = -gmtoff
|
||||
gmtoff_fraction = -gmtoff_fraction
|
||||
elif group_key == 'Z':
|
||||
# Since -1 is default value only need to worry about setting tz if
|
||||
# it can be something other than -1.
|
||||
found_zone = found_dict['Z'].lower()
|
||||
for value, tz_values in enumerate(locale_time.timezone):
|
||||
if found_zone in tz_values:
|
||||
# Deal with bad locale setup where timezone names are the
|
||||
# same and yet time.daylight is true; too ambiguous to
|
||||
# be able to tell what timezone has daylight savings
|
||||
if (time.tzname[0] == time.tzname[1] and
|
||||
time.daylight and found_zone not in ("utc", "gmt")):
|
||||
break
|
||||
else:
|
||||
tz = value
|
||||
break
|
||||
|
||||
# Deal with the cases where ambiguities arise
|
||||
# don't assume default values for ISO week/year
|
||||
if iso_year is not None:
|
||||
if julian is not None:
|
||||
raise ValueError("Day of the year directive '%j' is not "
|
||||
"compatible with ISO year directive '%G'. "
|
||||
"Use '%Y' instead.")
|
||||
elif iso_week is None or weekday is None:
|
||||
raise ValueError("ISO year directive '%G' must be used with "
|
||||
"the ISO week directive '%V' and a weekday "
|
||||
"directive ('%A', '%a', '%w', or '%u').")
|
||||
elif iso_week is not None:
|
||||
if year is None or weekday is None:
|
||||
raise ValueError("ISO week directive '%V' must be used with "
|
||||
"the ISO year directive '%G' and a weekday "
|
||||
"directive ('%A', '%a', '%w', or '%u').")
|
||||
else:
|
||||
raise ValueError("ISO week directive '%V' is incompatible with "
|
||||
"the year directive '%Y'. Use the ISO year '%G' "
|
||||
"instead.")
|
||||
|
||||
leap_year_fix = False
|
||||
if year is None:
|
||||
if month == 2 and day == 29:
|
||||
year = 1904 # 1904 is first leap year of 20th century
|
||||
leap_year_fix = True
|
||||
else:
|
||||
year = 1900
|
||||
|
||||
# If we know the week of the year and what day of that week, we can figure
|
||||
# out the Julian day of the year.
|
||||
if julian is None and weekday is not None:
|
||||
if week_of_year is not None:
|
||||
week_starts_Mon = True if week_of_year_start == 0 else False
|
||||
julian = _calc_julian_from_U_or_W(year, week_of_year, weekday,
|
||||
week_starts_Mon)
|
||||
elif iso_year is not None and iso_week is not None:
|
||||
datetime_result = datetime_date.fromisocalendar(iso_year, iso_week, weekday + 1)
|
||||
year = datetime_result.year
|
||||
month = datetime_result.month
|
||||
day = datetime_result.day
|
||||
if julian is not None and julian <= 0:
|
||||
year -= 1
|
||||
yday = 366 if calendar.isleap(year) else 365
|
||||
julian += yday
|
||||
|
||||
if julian is None:
|
||||
# Cannot pre-calculate datetime_date() since can change in Julian
|
||||
# calculation and thus could have different value for the day of
|
||||
# the week calculation.
|
||||
# Need to add 1 to result since first day of the year is 1, not 0.
|
||||
julian = datetime_date(year, month, day).toordinal() - \
|
||||
datetime_date(year, 1, 1).toordinal() + 1
|
||||
else: # Assume that if they bothered to include Julian day (or if it was
|
||||
# calculated above with year/week/weekday) it will be accurate.
|
||||
datetime_result = datetime_date.fromordinal(
|
||||
(julian - 1) +
|
||||
datetime_date(year, 1, 1).toordinal())
|
||||
year = datetime_result.year
|
||||
month = datetime_result.month
|
||||
day = datetime_result.day
|
||||
if weekday is None:
|
||||
weekday = datetime_date(year, month, day).weekday()
|
||||
# Add timezone info
|
||||
tzname = found_dict.get("Z")
|
||||
|
||||
if leap_year_fix:
|
||||
# the caller didn't supply a year but asked for Feb 29th. We couldn't
|
||||
# use the default of 1900 for computations. We set it back to ensure
|
||||
# that February 29th is smaller than March 1st.
|
||||
year = 1900
|
||||
|
||||
return (year, month, day,
|
||||
hour, minute, second,
|
||||
weekday, julian, tz, tzname, gmtoff), fraction, gmtoff_fraction
|
||||
|
||||
def _strptime_time(data_string, format="%a %b %d %H:%M:%S %Y"):
|
||||
"""Return a time struct based on the input string and the
|
||||
format string."""
|
||||
tt = _strptime(data_string, format)[0]
|
||||
return time.struct_time(tt[:time._STRUCT_TM_ITEMS])
|
||||
|
||||
def _strptime_datetime(cls, data_string, format="%a %b %d %H:%M:%S %Y"):
|
||||
"""Return a class cls instance based on the input string and the
|
||||
format string."""
|
||||
tt, fraction, gmtoff_fraction = _strptime(data_string, format)
|
||||
tzname, gmtoff = tt[-2:]
|
||||
args = tt[:6] + (fraction,)
|
||||
if gmtoff is not None:
|
||||
tzdelta = datetime_timedelta(seconds=gmtoff, microseconds=gmtoff_fraction)
|
||||
if tzname:
|
||||
tz = datetime_timezone(tzdelta, tzname)
|
||||
else:
|
||||
tz = datetime_timezone(tzdelta)
|
||||
args += (tz,)
|
||||
|
||||
return cls(*args)
|
||||
4
Lib/abc.py
vendored
4
Lib/abc.py
vendored
@@ -18,7 +18,7 @@ def abstractmethod(funcobj):
|
||||
|
||||
class C(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def my_abstract_method(self, ...):
|
||||
def my_abstract_method(self, arg1, arg2, argN):
|
||||
...
|
||||
"""
|
||||
funcobj.__isabstractmethod__ = True
|
||||
@@ -106,7 +106,7 @@ else:
|
||||
implementations defined by the registering ABC be callable (not
|
||||
even via super()).
|
||||
"""
|
||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||
def __new__(mcls, name, bases, namespace, /, **kwargs):
|
||||
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
||||
_abc_init(cls)
|
||||
return cls
|
||||
|
||||
951
Lib/aifc.py
vendored
951
Lib/aifc.py
vendored
@@ -1,951 +0,0 @@
|
||||
"""Stuff to parse AIFF-C and AIFF files.
|
||||
|
||||
Unless explicitly stated otherwise, the description below is true
|
||||
both for AIFF-C files and AIFF files.
|
||||
|
||||
An AIFF-C file has the following structure.
|
||||
|
||||
+-----------------+
|
||||
| FORM |
|
||||
+-----------------+
|
||||
| <size> |
|
||||
+----+------------+
|
||||
| | AIFC |
|
||||
| +------------+
|
||||
| | <chunks> |
|
||||
| | . |
|
||||
| | . |
|
||||
| | . |
|
||||
+----+------------+
|
||||
|
||||
An AIFF file has the string "AIFF" instead of "AIFC".
|
||||
|
||||
A chunk consists of an identifier (4 bytes) followed by a size (4 bytes,
|
||||
big endian order), followed by the data. The size field does not include
|
||||
the size of the 8 byte header.
|
||||
|
||||
The following chunk types are recognized.
|
||||
|
||||
FVER
|
||||
<version number of AIFF-C defining document> (AIFF-C only).
|
||||
MARK
|
||||
<# of markers> (2 bytes)
|
||||
list of markers:
|
||||
<marker ID> (2 bytes, must be > 0)
|
||||
<position> (4 bytes)
|
||||
<marker name> ("pstring")
|
||||
COMM
|
||||
<# of channels> (2 bytes)
|
||||
<# of sound frames> (4 bytes)
|
||||
<size of the samples> (2 bytes)
|
||||
<sampling frequency> (10 bytes, IEEE 80-bit extended
|
||||
floating point)
|
||||
in AIFF-C files only:
|
||||
<compression type> (4 bytes)
|
||||
<human-readable version of compression type> ("pstring")
|
||||
SSND
|
||||
<offset> (4 bytes, not used by this program)
|
||||
<blocksize> (4 bytes, not used by this program)
|
||||
<sound data>
|
||||
|
||||
A pstring consists of 1 byte length, a string of characters, and 0 or 1
|
||||
byte pad to make the total length even.
|
||||
|
||||
Usage.
|
||||
|
||||
Reading AIFF files:
|
||||
f = aifc.open(file, 'r')
|
||||
where file is either the name of a file or an open file pointer.
|
||||
The open file pointer must have methods read(), seek(), and close().
|
||||
In some types of audio files, if the setpos() method is not used,
|
||||
the seek() method is not necessary.
|
||||
|
||||
This returns an instance of a class with the following public methods:
|
||||
getnchannels() -- returns number of audio channels (1 for
|
||||
mono, 2 for stereo)
|
||||
getsampwidth() -- returns sample width in bytes
|
||||
getframerate() -- returns sampling frequency
|
||||
getnframes() -- returns number of audio frames
|
||||
getcomptype() -- returns compression type ('NONE' for AIFF files)
|
||||
getcompname() -- returns human-readable version of
|
||||
compression type ('not compressed' for AIFF files)
|
||||
getparams() -- returns a namedtuple consisting of all of the
|
||||
above in the above order
|
||||
getmarkers() -- get the list of marks in the audio file or None
|
||||
if there are no marks
|
||||
getmark(id) -- get mark with the specified id (raises an error
|
||||
if the mark does not exist)
|
||||
readframes(n) -- returns at most n frames of audio
|
||||
rewind() -- rewind to the beginning of the audio stream
|
||||
setpos(pos) -- seek to the specified position
|
||||
tell() -- return the current position
|
||||
close() -- close the instance (make it unusable)
|
||||
The position returned by tell(), the position given to setpos() and
|
||||
the position of marks are all compatible and have nothing to do with
|
||||
the actual position in the file.
|
||||
The close() method is called automatically when the class instance
|
||||
is destroyed.
|
||||
|
||||
Writing AIFF files:
|
||||
f = aifc.open(file, 'w')
|
||||
where file is either the name of a file or an open file pointer.
|
||||
The open file pointer must have methods write(), tell(), seek(), and
|
||||
close().
|
||||
|
||||
This returns an instance of a class with the following public methods:
|
||||
aiff() -- create an AIFF file (AIFF-C default)
|
||||
aifc() -- create an AIFF-C file
|
||||
setnchannels(n) -- set the number of channels
|
||||
setsampwidth(n) -- set the sample width
|
||||
setframerate(n) -- set the frame rate
|
||||
setnframes(n) -- set the number of frames
|
||||
setcomptype(type, name)
|
||||
-- set the compression type and the
|
||||
human-readable compression type
|
||||
setparams(tuple)
|
||||
-- set all parameters at once
|
||||
setmark(id, pos, name)
|
||||
-- add specified mark to the list of marks
|
||||
tell() -- return current position in output file (useful
|
||||
in combination with setmark())
|
||||
writeframesraw(data)
|
||||
-- write audio frames without pathing up the
|
||||
file header
|
||||
writeframes(data)
|
||||
-- write audio frames and patch up the file header
|
||||
close() -- patch up the file header and close the
|
||||
output file
|
||||
You should set the parameters before the first writeframesraw or
|
||||
writeframes. The total number of frames does not need to be set,
|
||||
but when it is set to the correct value, the header does not have to
|
||||
be patched up.
|
||||
It is best to first set all parameters, perhaps possibly the
|
||||
compression type, and then write audio frames using writeframesraw.
|
||||
When all frames have been written, either call writeframes(b'') or
|
||||
close() to patch up the sizes in the header.
|
||||
Marks can be added anytime. If there are any marks, you must call
|
||||
close() after all frames have been written.
|
||||
The close() method is called automatically when the class instance
|
||||
is destroyed.
|
||||
|
||||
When a file is opened with the extension '.aiff', an AIFF file is
|
||||
written, otherwise an AIFF-C file is written. This default can be
|
||||
changed by calling aiff() or aifc() before the first writeframes or
|
||||
writeframesraw.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import builtins
|
||||
import warnings
|
||||
|
||||
__all__ = ["Error", "open", "openfp"]
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
_AIFC_version = 0xA2805140 # Version 1 of AIFF-C
|
||||
|
||||
def _read_long(file):
|
||||
try:
|
||||
return struct.unpack('>l', file.read(4))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_ulong(file):
|
||||
try:
|
||||
return struct.unpack('>L', file.read(4))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_short(file):
|
||||
try:
|
||||
return struct.unpack('>h', file.read(2))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_ushort(file):
|
||||
try:
|
||||
return struct.unpack('>H', file.read(2))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_string(file):
|
||||
length = ord(file.read(1))
|
||||
if length == 0:
|
||||
data = b''
|
||||
else:
|
||||
data = file.read(length)
|
||||
if length & 1 == 0:
|
||||
dummy = file.read(1)
|
||||
return data
|
||||
|
||||
_HUGE_VAL = 1.79769313486231e+308 # See <limits.h>
|
||||
|
||||
def _read_float(f): # 10 bytes
|
||||
expon = _read_short(f) # 2 bytes
|
||||
sign = 1
|
||||
if expon < 0:
|
||||
sign = -1
|
||||
expon = expon + 0x8000
|
||||
himant = _read_ulong(f) # 4 bytes
|
||||
lomant = _read_ulong(f) # 4 bytes
|
||||
if expon == himant == lomant == 0:
|
||||
f = 0.0
|
||||
elif expon == 0x7FFF:
|
||||
f = _HUGE_VAL
|
||||
else:
|
||||
expon = expon - 16383
|
||||
f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
|
||||
return sign * f
|
||||
|
||||
def _write_short(f, x):
|
||||
f.write(struct.pack('>h', x))
|
||||
|
||||
def _write_ushort(f, x):
|
||||
f.write(struct.pack('>H', x))
|
||||
|
||||
def _write_long(f, x):
|
||||
f.write(struct.pack('>l', x))
|
||||
|
||||
def _write_ulong(f, x):
|
||||
f.write(struct.pack('>L', x))
|
||||
|
||||
def _write_string(f, s):
|
||||
if len(s) > 255:
|
||||
raise ValueError("string exceeds maximum pstring length")
|
||||
f.write(struct.pack('B', len(s)))
|
||||
f.write(s)
|
||||
if len(s) & 1 == 0:
|
||||
f.write(b'\x00')
|
||||
|
||||
def _write_float(f, x):
|
||||
import math
|
||||
if x < 0:
|
||||
sign = 0x8000
|
||||
x = x * -1
|
||||
else:
|
||||
sign = 0
|
||||
if x == 0:
|
||||
expon = 0
|
||||
himant = 0
|
||||
lomant = 0
|
||||
else:
|
||||
fmant, expon = math.frexp(x)
|
||||
if expon > 16384 or fmant >= 1 or fmant != fmant: # Infinity or NaN
|
||||
expon = sign|0x7FFF
|
||||
himant = 0
|
||||
lomant = 0
|
||||
else: # Finite
|
||||
expon = expon + 16382
|
||||
if expon < 0: # denormalized
|
||||
fmant = math.ldexp(fmant, expon)
|
||||
expon = 0
|
||||
expon = expon | sign
|
||||
fmant = math.ldexp(fmant, 32)
|
||||
fsmant = math.floor(fmant)
|
||||
himant = int(fsmant)
|
||||
fmant = math.ldexp(fmant - fsmant, 32)
|
||||
fsmant = math.floor(fmant)
|
||||
lomant = int(fsmant)
|
||||
_write_ushort(f, expon)
|
||||
_write_ulong(f, himant)
|
||||
_write_ulong(f, lomant)
|
||||
|
||||
from chunk import Chunk
|
||||
from collections import namedtuple
|
||||
|
||||
_aifc_params = namedtuple('_aifc_params',
|
||||
'nchannels sampwidth framerate nframes comptype compname')
|
||||
|
||||
_aifc_params.nchannels.__doc__ = 'Number of audio channels (1 for mono, 2 for stereo)'
|
||||
_aifc_params.sampwidth.__doc__ = 'Sample width in bytes'
|
||||
_aifc_params.framerate.__doc__ = 'Sampling frequency'
|
||||
_aifc_params.nframes.__doc__ = 'Number of audio frames'
|
||||
_aifc_params.comptype.__doc__ = 'Compression type ("NONE" for AIFF files)'
|
||||
_aifc_params.compname.__doc__ = ("""\
|
||||
A human-readable version of the compression type
|
||||
('not compressed' for AIFF files)""")
|
||||
|
||||
|
||||
class Aifc_read:
|
||||
# Variables used in this class:
|
||||
#
|
||||
# These variables are available to the user though appropriate
|
||||
# methods of this class:
|
||||
# _file -- the open file with methods read(), close(), and seek()
|
||||
# set through the __init__() method
|
||||
# _nchannels -- the number of audio channels
|
||||
# available through the getnchannels() method
|
||||
# _nframes -- the number of audio frames
|
||||
# available through the getnframes() method
|
||||
# _sampwidth -- the number of bytes per audio sample
|
||||
# available through the getsampwidth() method
|
||||
# _framerate -- the sampling frequency
|
||||
# available through the getframerate() method
|
||||
# _comptype -- the AIFF-C compression type ('NONE' if AIFF)
|
||||
# available through the getcomptype() method
|
||||
# _compname -- the human-readable AIFF-C compression type
|
||||
# available through the getcomptype() method
|
||||
# _markers -- the marks in the audio file
|
||||
# available through the getmarkers() and getmark()
|
||||
# methods
|
||||
# _soundpos -- the position in the audio stream
|
||||
# available through the tell() method, set through the
|
||||
# setpos() method
|
||||
#
|
||||
# These variables are used internally only:
|
||||
# _version -- the AIFF-C version number
|
||||
# _decomp -- the decompressor from builtin module cl
|
||||
# _comm_chunk_read -- 1 iff the COMM chunk has been read
|
||||
# _aifc -- 1 iff reading an AIFF-C file
|
||||
# _ssnd_seek_needed -- 1 iff positioned correctly in audio
|
||||
# file for readframes()
|
||||
# _ssnd_chunk -- instantiation of a chunk class for the SSND chunk
|
||||
# _framesize -- size of one frame in the file
|
||||
|
||||
_file = None # Set here since __del__ checks it
|
||||
|
||||
def initfp(self, file):
|
||||
self._version = 0
|
||||
self._convert = None
|
||||
self._markers = []
|
||||
self._soundpos = 0
|
||||
self._file = file
|
||||
chunk = Chunk(file)
|
||||
if chunk.getname() != b'FORM':
|
||||
raise Error('file does not start with FORM id')
|
||||
formdata = chunk.read(4)
|
||||
if formdata == b'AIFF':
|
||||
self._aifc = 0
|
||||
elif formdata == b'AIFC':
|
||||
self._aifc = 1
|
||||
else:
|
||||
raise Error('not an AIFF or AIFF-C file')
|
||||
self._comm_chunk_read = 0
|
||||
self._ssnd_chunk = None
|
||||
while 1:
|
||||
self._ssnd_seek_needed = 1
|
||||
try:
|
||||
chunk = Chunk(self._file)
|
||||
except EOFError:
|
||||
break
|
||||
chunkname = chunk.getname()
|
||||
if chunkname == b'COMM':
|
||||
self._read_comm_chunk(chunk)
|
||||
self._comm_chunk_read = 1
|
||||
elif chunkname == b'SSND':
|
||||
self._ssnd_chunk = chunk
|
||||
dummy = chunk.read(8)
|
||||
self._ssnd_seek_needed = 0
|
||||
elif chunkname == b'FVER':
|
||||
self._version = _read_ulong(chunk)
|
||||
elif chunkname == b'MARK':
|
||||
self._readmark(chunk)
|
||||
chunk.skip()
|
||||
if not self._comm_chunk_read or not self._ssnd_chunk:
|
||||
raise Error('COMM chunk and/or SSND chunk missing')
|
||||
|
||||
def __init__(self, f):
|
||||
if isinstance(f, str):
|
||||
file_object = builtins.open(f, 'rb')
|
||||
try:
|
||||
self.initfp(file_object)
|
||||
except:
|
||||
file_object.close()
|
||||
raise
|
||||
else:
|
||||
# assume it is an open file object already
|
||||
self.initfp(f)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
#
|
||||
# User visible methods.
|
||||
#
|
||||
def getfp(self):
|
||||
return self._file
|
||||
|
||||
def rewind(self):
|
||||
self._ssnd_seek_needed = 1
|
||||
self._soundpos = 0
|
||||
|
||||
def close(self):
|
||||
file = self._file
|
||||
if file is not None:
|
||||
self._file = None
|
||||
file.close()
|
||||
|
||||
def tell(self):
|
||||
return self._soundpos
|
||||
|
||||
def getnchannels(self):
|
||||
return self._nchannels
|
||||
|
||||
def getnframes(self):
|
||||
return self._nframes
|
||||
|
||||
def getsampwidth(self):
|
||||
return self._sampwidth
|
||||
|
||||
def getframerate(self):
|
||||
return self._framerate
|
||||
|
||||
def getcomptype(self):
|
||||
return self._comptype
|
||||
|
||||
def getcompname(self):
|
||||
return self._compname
|
||||
|
||||
## def getversion(self):
|
||||
## return self._version
|
||||
|
||||
def getparams(self):
|
||||
return _aifc_params(self.getnchannels(), self.getsampwidth(),
|
||||
self.getframerate(), self.getnframes(),
|
||||
self.getcomptype(), self.getcompname())
|
||||
|
||||
def getmarkers(self):
|
||||
if len(self._markers) == 0:
|
||||
return None
|
||||
return self._markers
|
||||
|
||||
def getmark(self, id):
|
||||
for marker in self._markers:
|
||||
if id == marker[0]:
|
||||
return marker
|
||||
raise Error('marker {0!r} does not exist'.format(id))
|
||||
|
||||
def setpos(self, pos):
|
||||
if pos < 0 or pos > self._nframes:
|
||||
raise Error('position not in range')
|
||||
self._soundpos = pos
|
||||
self._ssnd_seek_needed = 1
|
||||
|
||||
def readframes(self, nframes):
|
||||
if self._ssnd_seek_needed:
|
||||
self._ssnd_chunk.seek(0)
|
||||
dummy = self._ssnd_chunk.read(8)
|
||||
pos = self._soundpos * self._framesize
|
||||
if pos:
|
||||
self._ssnd_chunk.seek(pos + 8)
|
||||
self._ssnd_seek_needed = 0
|
||||
if nframes == 0:
|
||||
return b''
|
||||
data = self._ssnd_chunk.read(nframes * self._framesize)
|
||||
if self._convert and data:
|
||||
data = self._convert(data)
|
||||
self._soundpos = self._soundpos + len(data) // (self._nchannels
|
||||
* self._sampwidth)
|
||||
return data
|
||||
|
||||
#
|
||||
# Internal methods.
|
||||
#
|
||||
|
||||
def _alaw2lin(self, data):
|
||||
import audioop
|
||||
return audioop.alaw2lin(data, 2)
|
||||
|
||||
def _ulaw2lin(self, data):
|
||||
import audioop
|
||||
return audioop.ulaw2lin(data, 2)
|
||||
|
||||
def _adpcm2lin(self, data):
|
||||
import audioop
|
||||
if not hasattr(self, '_adpcmstate'):
|
||||
# first time
|
||||
self._adpcmstate = None
|
||||
data, self._adpcmstate = audioop.adpcm2lin(data, 2, self._adpcmstate)
|
||||
return data
|
||||
|
||||
def _read_comm_chunk(self, chunk):
|
||||
self._nchannels = _read_short(chunk)
|
||||
self._nframes = _read_long(chunk)
|
||||
self._sampwidth = (_read_short(chunk) + 7) // 8
|
||||
self._framerate = int(_read_float(chunk))
|
||||
if self._sampwidth <= 0:
|
||||
raise Error('bad sample width')
|
||||
if self._nchannels <= 0:
|
||||
raise Error('bad # of channels')
|
||||
self._framesize = self._nchannels * self._sampwidth
|
||||
if self._aifc:
|
||||
#DEBUG: SGI's soundeditor produces a bad size :-(
|
||||
kludge = 0
|
||||
if chunk.chunksize == 18:
|
||||
kludge = 1
|
||||
warnings.warn('Warning: bad COMM chunk size')
|
||||
chunk.chunksize = 23
|
||||
#DEBUG end
|
||||
self._comptype = chunk.read(4)
|
||||
#DEBUG start
|
||||
if kludge:
|
||||
length = ord(chunk.file.read(1))
|
||||
if length & 1 == 0:
|
||||
length = length + 1
|
||||
chunk.chunksize = chunk.chunksize + length
|
||||
chunk.file.seek(-1, 1)
|
||||
#DEBUG end
|
||||
self._compname = _read_string(chunk)
|
||||
if self._comptype != b'NONE':
|
||||
if self._comptype == b'G722':
|
||||
self._convert = self._adpcm2lin
|
||||
elif self._comptype in (b'ulaw', b'ULAW'):
|
||||
self._convert = self._ulaw2lin
|
||||
elif self._comptype in (b'alaw', b'ALAW'):
|
||||
self._convert = self._alaw2lin
|
||||
else:
|
||||
raise Error('unsupported compression type')
|
||||
self._sampwidth = 2
|
||||
else:
|
||||
self._comptype = b'NONE'
|
||||
self._compname = b'not compressed'
|
||||
|
||||
def _readmark(self, chunk):
|
||||
nmarkers = _read_short(chunk)
|
||||
# Some files appear to contain invalid counts.
|
||||
# Cope with this by testing for EOF.
|
||||
try:
|
||||
for i in range(nmarkers):
|
||||
id = _read_short(chunk)
|
||||
pos = _read_long(chunk)
|
||||
name = _read_string(chunk)
|
||||
if pos or name:
|
||||
# some files appear to have
|
||||
# dummy markers consisting of
|
||||
# a position 0 and name ''
|
||||
self._markers.append((id, pos, name))
|
||||
except EOFError:
|
||||
w = ('Warning: MARK chunk contains only %s marker%s instead of %s' %
|
||||
(len(self._markers), '' if len(self._markers) == 1 else 's',
|
||||
nmarkers))
|
||||
warnings.warn(w)
|
||||
|
||||
class Aifc_write:
|
||||
# Variables used in this class:
|
||||
#
|
||||
# These variables are user settable through appropriate methods
|
||||
# of this class:
|
||||
# _file -- the open file with methods write(), close(), tell(), seek()
|
||||
# set through the __init__() method
|
||||
# _comptype -- the AIFF-C compression type ('NONE' in AIFF)
|
||||
# set through the setcomptype() or setparams() method
|
||||
# _compname -- the human-readable AIFF-C compression type
|
||||
# set through the setcomptype() or setparams() method
|
||||
# _nchannels -- the number of audio channels
|
||||
# set through the setnchannels() or setparams() method
|
||||
# _sampwidth -- the number of bytes per audio sample
|
||||
# set through the setsampwidth() or setparams() method
|
||||
# _framerate -- the sampling frequency
|
||||
# set through the setframerate() or setparams() method
|
||||
# _nframes -- the number of audio frames written to the header
|
||||
# set through the setnframes() or setparams() method
|
||||
# _aifc -- whether we're writing an AIFF-C file or an AIFF file
|
||||
# set through the aifc() method, reset through the
|
||||
# aiff() method
|
||||
#
|
||||
# These variables are used internally only:
|
||||
# _version -- the AIFF-C version number
|
||||
# _comp -- the compressor from builtin module cl
|
||||
# _nframeswritten -- the number of audio frames actually written
|
||||
# _datalength -- the size of the audio samples written to the header
|
||||
# _datawritten -- the size of the audio samples actually written
|
||||
|
||||
_file = None # Set here since __del__ checks it
|
||||
|
||||
def __init__(self, f):
|
||||
if isinstance(f, str):
|
||||
file_object = builtins.open(f, 'wb')
|
||||
try:
|
||||
self.initfp(file_object)
|
||||
except:
|
||||
file_object.close()
|
||||
raise
|
||||
|
||||
# treat .aiff file extensions as non-compressed audio
|
||||
if f.endswith('.aiff'):
|
||||
self._aifc = 0
|
||||
else:
|
||||
# assume it is an open file object already
|
||||
self.initfp(f)
|
||||
|
||||
def initfp(self, file):
|
||||
self._file = file
|
||||
self._version = _AIFC_version
|
||||
self._comptype = b'NONE'
|
||||
self._compname = b'not compressed'
|
||||
self._convert = None
|
||||
self._nchannels = 0
|
||||
self._sampwidth = 0
|
||||
self._framerate = 0
|
||||
self._nframes = 0
|
||||
self._nframeswritten = 0
|
||||
self._datawritten = 0
|
||||
self._datalength = 0
|
||||
self._markers = []
|
||||
self._marklength = 0
|
||||
self._aifc = 1 # AIFF-C is default
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
#
|
||||
# User visible methods.
|
||||
#
|
||||
def aiff(self):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
self._aifc = 0
|
||||
|
||||
def aifc(self):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
self._aifc = 1
|
||||
|
||||
def setnchannels(self, nchannels):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if nchannels < 1:
|
||||
raise Error('bad # of channels')
|
||||
self._nchannels = nchannels
|
||||
|
||||
def getnchannels(self):
|
||||
if not self._nchannels:
|
||||
raise Error('number of channels not set')
|
||||
return self._nchannels
|
||||
|
||||
def setsampwidth(self, sampwidth):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if sampwidth < 1 or sampwidth > 4:
|
||||
raise Error('bad sample width')
|
||||
self._sampwidth = sampwidth
|
||||
|
||||
def getsampwidth(self):
|
||||
if not self._sampwidth:
|
||||
raise Error('sample width not set')
|
||||
return self._sampwidth
|
||||
|
||||
def setframerate(self, framerate):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if framerate <= 0:
|
||||
raise Error('bad frame rate')
|
||||
self._framerate = framerate
|
||||
|
||||
def getframerate(self):
|
||||
if not self._framerate:
|
||||
raise Error('frame rate not set')
|
||||
return self._framerate
|
||||
|
||||
def setnframes(self, nframes):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
self._nframes = nframes
|
||||
|
||||
def getnframes(self):
|
||||
return self._nframeswritten
|
||||
|
||||
def setcomptype(self, comptype, compname):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if comptype not in (b'NONE', b'ulaw', b'ULAW',
|
||||
b'alaw', b'ALAW', b'G722'):
|
||||
raise Error('unsupported compression type')
|
||||
self._comptype = comptype
|
||||
self._compname = compname
|
||||
|
||||
def getcomptype(self):
|
||||
return self._comptype
|
||||
|
||||
def getcompname(self):
|
||||
return self._compname
|
||||
|
||||
## def setversion(self, version):
|
||||
## if self._nframeswritten:
|
||||
## raise Error, 'cannot change parameters after starting to write'
|
||||
## self._version = version
|
||||
|
||||
def setparams(self, params):
|
||||
nchannels, sampwidth, framerate, nframes, comptype, compname = params
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if comptype not in (b'NONE', b'ulaw', b'ULAW',
|
||||
b'alaw', b'ALAW', b'G722'):
|
||||
raise Error('unsupported compression type')
|
||||
self.setnchannels(nchannels)
|
||||
self.setsampwidth(sampwidth)
|
||||
self.setframerate(framerate)
|
||||
self.setnframes(nframes)
|
||||
self.setcomptype(comptype, compname)
|
||||
|
||||
def getparams(self):
|
||||
if not self._nchannels or not self._sampwidth or not self._framerate:
|
||||
raise Error('not all parameters set')
|
||||
return _aifc_params(self._nchannels, self._sampwidth, self._framerate,
|
||||
self._nframes, self._comptype, self._compname)
|
||||
|
||||
def setmark(self, id, pos, name):
|
||||
if id <= 0:
|
||||
raise Error('marker ID must be > 0')
|
||||
if pos < 0:
|
||||
raise Error('marker position must be >= 0')
|
||||
if not isinstance(name, bytes):
|
||||
raise Error('marker name must be bytes')
|
||||
for i in range(len(self._markers)):
|
||||
if id == self._markers[i][0]:
|
||||
self._markers[i] = id, pos, name
|
||||
return
|
||||
self._markers.append((id, pos, name))
|
||||
|
||||
def getmark(self, id):
|
||||
for marker in self._markers:
|
||||
if id == marker[0]:
|
||||
return marker
|
||||
raise Error('marker {0!r} does not exist'.format(id))
|
||||
|
||||
def getmarkers(self):
|
||||
if len(self._markers) == 0:
|
||||
return None
|
||||
return self._markers
|
||||
|
||||
def tell(self):
|
||||
return self._nframeswritten
|
||||
|
||||
def writeframesraw(self, data):
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
data = memoryview(data).cast('B')
|
||||
self._ensure_header_written(len(data))
|
||||
nframes = len(data) // (self._sampwidth * self._nchannels)
|
||||
if self._convert:
|
||||
data = self._convert(data)
|
||||
self._file.write(data)
|
||||
self._nframeswritten = self._nframeswritten + nframes
|
||||
self._datawritten = self._datawritten + len(data)
|
||||
|
||||
def writeframes(self, data):
|
||||
self.writeframesraw(data)
|
||||
if self._nframeswritten != self._nframes or \
|
||||
self._datalength != self._datawritten:
|
||||
self._patchheader()
|
||||
|
||||
def close(self):
|
||||
if self._file is None:
|
||||
return
|
||||
try:
|
||||
self._ensure_header_written(0)
|
||||
if self._datawritten & 1:
|
||||
# quick pad to even size
|
||||
self._file.write(b'\x00')
|
||||
self._datawritten = self._datawritten + 1
|
||||
self._writemarkers()
|
||||
if self._nframeswritten != self._nframes or \
|
||||
self._datalength != self._datawritten or \
|
||||
self._marklength:
|
||||
self._patchheader()
|
||||
finally:
|
||||
# Prevent ref cycles
|
||||
self._convert = None
|
||||
f = self._file
|
||||
self._file = None
|
||||
f.close()
|
||||
|
||||
#
|
||||
# Internal methods.
|
||||
#
|
||||
|
||||
def _lin2alaw(self, data):
|
||||
import audioop
|
||||
return audioop.lin2alaw(data, 2)
|
||||
|
||||
def _lin2ulaw(self, data):
|
||||
import audioop
|
||||
return audioop.lin2ulaw(data, 2)
|
||||
|
||||
def _lin2adpcm(self, data):
|
||||
import audioop
|
||||
if not hasattr(self, '_adpcmstate'):
|
||||
self._adpcmstate = None
|
||||
data, self._adpcmstate = audioop.lin2adpcm(data, 2, self._adpcmstate)
|
||||
return data
|
||||
|
||||
def _ensure_header_written(self, datasize):
|
||||
if not self._nframeswritten:
|
||||
if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
|
||||
if not self._sampwidth:
|
||||
self._sampwidth = 2
|
||||
if self._sampwidth != 2:
|
||||
raise Error('sample width must be 2 when compressing '
|
||||
'with ulaw/ULAW, alaw/ALAW or G7.22 (ADPCM)')
|
||||
if not self._nchannels:
|
||||
raise Error('# channels not specified')
|
||||
if not self._sampwidth:
|
||||
raise Error('sample width not specified')
|
||||
if not self._framerate:
|
||||
raise Error('sampling rate not specified')
|
||||
self._write_header(datasize)
|
||||
|
||||
def _init_compression(self):
|
||||
if self._comptype == b'G722':
|
||||
self._convert = self._lin2adpcm
|
||||
elif self._comptype in (b'ulaw', b'ULAW'):
|
||||
self._convert = self._lin2ulaw
|
||||
elif self._comptype in (b'alaw', b'ALAW'):
|
||||
self._convert = self._lin2alaw
|
||||
|
||||
def _write_header(self, initlength):
|
||||
if self._aifc and self._comptype != b'NONE':
|
||||
self._init_compression()
|
||||
self._file.write(b'FORM')
|
||||
if not self._nframes:
|
||||
self._nframes = initlength // (self._nchannels * self._sampwidth)
|
||||
self._datalength = self._nframes * self._nchannels * self._sampwidth
|
||||
if self._datalength & 1:
|
||||
self._datalength = self._datalength + 1
|
||||
if self._aifc:
|
||||
if self._comptype in (b'ulaw', b'ULAW', b'alaw', b'ALAW'):
|
||||
self._datalength = self._datalength // 2
|
||||
if self._datalength & 1:
|
||||
self._datalength = self._datalength + 1
|
||||
elif self._comptype == b'G722':
|
||||
self._datalength = (self._datalength + 3) // 4
|
||||
if self._datalength & 1:
|
||||
self._datalength = self._datalength + 1
|
||||
try:
|
||||
self._form_length_pos = self._file.tell()
|
||||
except (AttributeError, OSError):
|
||||
self._form_length_pos = None
|
||||
commlength = self._write_form_length(self._datalength)
|
||||
if self._aifc:
|
||||
self._file.write(b'AIFC')
|
||||
self._file.write(b'FVER')
|
||||
_write_ulong(self._file, 4)
|
||||
_write_ulong(self._file, self._version)
|
||||
else:
|
||||
self._file.write(b'AIFF')
|
||||
self._file.write(b'COMM')
|
||||
_write_ulong(self._file, commlength)
|
||||
_write_short(self._file, self._nchannels)
|
||||
if self._form_length_pos is not None:
|
||||
self._nframes_pos = self._file.tell()
|
||||
_write_ulong(self._file, self._nframes)
|
||||
if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
|
||||
_write_short(self._file, 8)
|
||||
else:
|
||||
_write_short(self._file, self._sampwidth * 8)
|
||||
_write_float(self._file, self._framerate)
|
||||
if self._aifc:
|
||||
self._file.write(self._comptype)
|
||||
_write_string(self._file, self._compname)
|
||||
self._file.write(b'SSND')
|
||||
if self._form_length_pos is not None:
|
||||
self._ssnd_length_pos = self._file.tell()
|
||||
_write_ulong(self._file, self._datalength + 8)
|
||||
_write_ulong(self._file, 0)
|
||||
_write_ulong(self._file, 0)
|
||||
|
||||
def _write_form_length(self, datalength):
|
||||
if self._aifc:
|
||||
commlength = 18 + 5 + len(self._compname)
|
||||
if commlength & 1:
|
||||
commlength = commlength + 1
|
||||
verslength = 12
|
||||
else:
|
||||
commlength = 18
|
||||
verslength = 0
|
||||
_write_ulong(self._file, 4 + verslength + self._marklength + \
|
||||
8 + commlength + 16 + datalength)
|
||||
return commlength
|
||||
|
||||
def _patchheader(self):
|
||||
curpos = self._file.tell()
|
||||
if self._datawritten & 1:
|
||||
datalength = self._datawritten + 1
|
||||
self._file.write(b'\x00')
|
||||
else:
|
||||
datalength = self._datawritten
|
||||
if datalength == self._datalength and \
|
||||
self._nframes == self._nframeswritten and \
|
||||
self._marklength == 0:
|
||||
self._file.seek(curpos, 0)
|
||||
return
|
||||
self._file.seek(self._form_length_pos, 0)
|
||||
dummy = self._write_form_length(datalength)
|
||||
self._file.seek(self._nframes_pos, 0)
|
||||
_write_ulong(self._file, self._nframeswritten)
|
||||
self._file.seek(self._ssnd_length_pos, 0)
|
||||
_write_ulong(self._file, datalength + 8)
|
||||
self._file.seek(curpos, 0)
|
||||
self._nframes = self._nframeswritten
|
||||
self._datalength = datalength
|
||||
|
||||
def _writemarkers(self):
|
||||
if len(self._markers) == 0:
|
||||
return
|
||||
self._file.write(b'MARK')
|
||||
length = 2
|
||||
for marker in self._markers:
|
||||
id, pos, name = marker
|
||||
length = length + len(name) + 1 + 6
|
||||
if len(name) & 1 == 0:
|
||||
length = length + 1
|
||||
_write_ulong(self._file, length)
|
||||
self._marklength = length + 8
|
||||
_write_short(self._file, len(self._markers))
|
||||
for marker in self._markers:
|
||||
id, pos, name = marker
|
||||
_write_short(self._file, id)
|
||||
_write_ulong(self._file, pos)
|
||||
_write_string(self._file, name)
|
||||
|
||||
def open(f, mode=None):
|
||||
if mode is None:
|
||||
if hasattr(f, 'mode'):
|
||||
mode = f.mode
|
||||
else:
|
||||
mode = 'rb'
|
||||
if mode in ('r', 'rb'):
|
||||
return Aifc_read(f)
|
||||
elif mode in ('w', 'wb'):
|
||||
return Aifc_write(f)
|
||||
else:
|
||||
raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
|
||||
|
||||
def openfp(f, mode=None):
|
||||
warnings.warn("aifc.openfp is deprecated since Python 3.7. "
|
||||
"Use aifc.open instead.", DeprecationWarning, stacklevel=2)
|
||||
return open(f, mode=mode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if not sys.argv[1:]:
|
||||
sys.argv.append('/usr/demos/data/audio/bach.aiff')
|
||||
fn = sys.argv[1]
|
||||
with open(fn, 'r') as f:
|
||||
print("Reading", fn)
|
||||
print("nchannels =", f.getnchannels())
|
||||
print("nframes =", f.getnframes())
|
||||
print("sampwidth =", f.getsampwidth())
|
||||
print("framerate =", f.getframerate())
|
||||
print("comptype =", f.getcomptype())
|
||||
print("compname =", f.getcompname())
|
||||
if sys.argv[2:]:
|
||||
gn = sys.argv[2]
|
||||
print("Writing", gn)
|
||||
with open(gn, 'w') as g:
|
||||
g.setparams(f.getparams())
|
||||
while 1:
|
||||
data = f.readframes(1024)
|
||||
if not data:
|
||||
break
|
||||
g.writeframes(data)
|
||||
print("Done.")
|
||||
4
Lib/antigravity.py
vendored
4
Lib/antigravity.py
vendored
@@ -11,7 +11,7 @@ def geohash(latitude, longitude, datedow):
|
||||
37.857713 -122.544543
|
||||
|
||||
'''
|
||||
# http://xkcd.com/426/
|
||||
h = hashlib.md5(datedow).hexdigest()
|
||||
# https://xkcd.com/426/
|
||||
h = hashlib.md5(datedow, usedforsecurity=False).hexdigest()
|
||||
p, q = [('%f' % float.fromhex('0.' + x)) for x in (h[:16], h[16:32])]
|
||||
print('%d%s %d%s' % (latitude, p[1:], longitude, q[1:]))
|
||||
|
||||
130
Lib/argparse.py
vendored
130
Lib/argparse.py
vendored
@@ -89,6 +89,8 @@ import os as _os
|
||||
import re as _re
|
||||
import sys as _sys
|
||||
|
||||
import warnings
|
||||
|
||||
from gettext import gettext as _, ngettext
|
||||
|
||||
SUPPRESS = '==SUPPRESS=='
|
||||
@@ -151,6 +153,7 @@ def _copy_items(items):
|
||||
# Formatting Help
|
||||
# ===============
|
||||
|
||||
|
||||
class HelpFormatter(object):
|
||||
"""Formatter for generating usage messages and argument help strings.
|
||||
|
||||
@@ -342,21 +345,22 @@ class HelpFormatter(object):
|
||||
def get_lines(parts, indent, prefix=None):
|
||||
lines = []
|
||||
line = []
|
||||
indent_length = len(indent)
|
||||
if prefix is not None:
|
||||
line_len = len(prefix) - 1
|
||||
else:
|
||||
line_len = len(indent) - 1
|
||||
line_len = indent_length - 1
|
||||
for part in parts:
|
||||
if line_len + 1 + len(part) > text_width and line:
|
||||
lines.append(indent + ' '.join(line))
|
||||
line = []
|
||||
line_len = len(indent) - 1
|
||||
line_len = indent_length - 1
|
||||
line.append(part)
|
||||
line_len += len(part) + 1
|
||||
if line:
|
||||
lines.append(indent + ' '.join(line))
|
||||
if prefix is not None:
|
||||
lines[0] = lines[0][len(indent):]
|
||||
lines[0] = lines[0][indent_length:]
|
||||
return lines
|
||||
|
||||
# if prog is short, follow it with optionals or positionals
|
||||
@@ -400,10 +404,18 @@ class HelpFormatter(object):
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
end = start + len(group._group_actions)
|
||||
group_action_count = len(group._group_actions)
|
||||
end = start + group_action_count
|
||||
if actions[start:end] == group._group_actions:
|
||||
|
||||
suppressed_actions_count = 0
|
||||
for action in group._group_actions:
|
||||
group_actions.add(action)
|
||||
if action.help is SUPPRESS:
|
||||
suppressed_actions_count += 1
|
||||
|
||||
exposed_actions_count = group_action_count - suppressed_actions_count
|
||||
|
||||
if not group.required:
|
||||
if start in inserts:
|
||||
inserts[start] += ' ['
|
||||
@@ -413,7 +425,7 @@ class HelpFormatter(object):
|
||||
inserts[end] += ']'
|
||||
else:
|
||||
inserts[end] = ']'
|
||||
else:
|
||||
elif exposed_actions_count > 1:
|
||||
if start in inserts:
|
||||
inserts[start] += ' ('
|
||||
else:
|
||||
@@ -487,7 +499,6 @@ class HelpFormatter(object):
|
||||
text = _re.sub(r'(%s) ' % open, r'\1', text)
|
||||
text = _re.sub(r' (%s)' % close, r'\1', text)
|
||||
text = _re.sub(r'%s *%s' % (open, close), r'', text)
|
||||
text = _re.sub(r'\(([^|]*)\)', r'\1', text)
|
||||
text = text.strip()
|
||||
|
||||
# return the text
|
||||
@@ -693,8 +704,19 @@ class ArgumentDefaultsHelpFormatter(HelpFormatter):
|
||||
"""
|
||||
|
||||
def _get_help_string(self, action):
|
||||
"""
|
||||
Add the default value to the option help message.
|
||||
|
||||
ArgumentDefaultsHelpFormatter and BooleanOptionalAction when it isn't
|
||||
already present. This code will do that, detecting cornercases to
|
||||
prevent duplicates or cases where it wouldn't make sense to the end
|
||||
user.
|
||||
"""
|
||||
help = action.help
|
||||
if '%(default)' not in action.help:
|
||||
if help is None:
|
||||
help = ''
|
||||
|
||||
if '%(default)' not in help:
|
||||
if action.default is not SUPPRESS:
|
||||
defaulting_nargs = [OPTIONAL, ZERO_OR_MORE]
|
||||
if action.option_strings or action.nargs in defaulting_nargs:
|
||||
@@ -702,6 +724,7 @@ class ArgumentDefaultsHelpFormatter(HelpFormatter):
|
||||
return help
|
||||
|
||||
|
||||
|
||||
class MetavarTypeHelpFormatter(HelpFormatter):
|
||||
"""Help message formatter which uses the argument 'type' as the default
|
||||
metavar value (instead of the argument 'dest')
|
||||
@@ -717,7 +740,6 @@ class MetavarTypeHelpFormatter(HelpFormatter):
|
||||
return action.type.__name__
|
||||
|
||||
|
||||
|
||||
# =====================
|
||||
# Options and Arguments
|
||||
# =====================
|
||||
@@ -752,7 +774,7 @@ class ArgumentError(Exception):
|
||||
if self.argument_name is None:
|
||||
format = '%(message)s'
|
||||
else:
|
||||
format = 'argument %(argument_name)s: %(message)s'
|
||||
format = _('argument %(argument_name)s: %(message)s')
|
||||
return format % dict(message=self.message,
|
||||
argument_name=self.argument_name)
|
||||
|
||||
@@ -860,16 +882,20 @@ class Action(_AttributeHolder):
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
raise NotImplementedError(_('.__call__() not defined'))
|
||||
|
||||
|
||||
# FIXME: remove together with `BooleanOptionalAction` deprecated arguments.
|
||||
_deprecated_default = object()
|
||||
|
||||
class BooleanOptionalAction(Action):
|
||||
def __init__(self,
|
||||
option_strings,
|
||||
dest,
|
||||
default=None,
|
||||
type=None,
|
||||
choices=None,
|
||||
type=_deprecated_default,
|
||||
choices=_deprecated_default,
|
||||
required=False,
|
||||
help=None,
|
||||
metavar=None):
|
||||
metavar=_deprecated_default):
|
||||
|
||||
_option_strings = []
|
||||
for option_string in option_strings:
|
||||
@@ -879,8 +905,23 @@ class BooleanOptionalAction(Action):
|
||||
option_string = '--no-' + option_string[2:]
|
||||
_option_strings.append(option_string)
|
||||
|
||||
if help is not None and default is not None and default is not SUPPRESS:
|
||||
help += " (default: %(default)s)"
|
||||
# We need `_deprecated` special value to ban explicit arguments that
|
||||
# match default value. Like:
|
||||
# parser.add_argument('-f', action=BooleanOptionalAction, type=int)
|
||||
for field_name in ('type', 'choices', 'metavar'):
|
||||
if locals()[field_name] is not _deprecated_default:
|
||||
warnings._deprecated(
|
||||
field_name,
|
||||
"{name!r} is deprecated as of Python 3.12 and will be "
|
||||
"removed in Python {remove}.",
|
||||
remove=(3, 14))
|
||||
|
||||
if type is _deprecated_default:
|
||||
type = None
|
||||
if choices is _deprecated_default:
|
||||
choices = None
|
||||
if metavar is _deprecated_default:
|
||||
metavar = None
|
||||
|
||||
super().__init__(
|
||||
option_strings=_option_strings,
|
||||
@@ -893,6 +934,7 @@ class BooleanOptionalAction(Action):
|
||||
help=help,
|
||||
metavar=metavar)
|
||||
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
if option_string in self.option_strings:
|
||||
setattr(namespace, self.dest, not option_string.startswith('--no-'))
|
||||
@@ -941,7 +983,7 @@ class _StoreConstAction(Action):
|
||||
def __init__(self,
|
||||
option_strings,
|
||||
dest,
|
||||
const,
|
||||
const=None,
|
||||
default=None,
|
||||
required=False,
|
||||
help=None,
|
||||
@@ -1036,7 +1078,7 @@ class _AppendConstAction(Action):
|
||||
def __init__(self,
|
||||
option_strings,
|
||||
dest,
|
||||
const,
|
||||
const=None,
|
||||
default=None,
|
||||
required=False,
|
||||
help=None,
|
||||
@@ -1168,6 +1210,13 @@ class _SubParsersAction(Action):
|
||||
|
||||
aliases = kwargs.pop('aliases', ())
|
||||
|
||||
if name in self._name_parser_map:
|
||||
raise ArgumentError(self, _('conflicting subparser: %s') % name)
|
||||
for alias in aliases:
|
||||
if alias in self._name_parser_map:
|
||||
raise ArgumentError(
|
||||
self, _('conflicting subparser alias: %s') % alias)
|
||||
|
||||
# create a pseudo-action to hold the choice help
|
||||
if 'help' in kwargs:
|
||||
help = kwargs.pop('help')
|
||||
@@ -1648,6 +1697,14 @@ class _ArgumentGroup(_ActionsContainer):
|
||||
super(_ArgumentGroup, self)._remove_action(action)
|
||||
self._group_actions.remove(action)
|
||||
|
||||
def add_argument_group(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"Nesting argument groups is deprecated.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return super().add_argument_group(*args, **kwargs)
|
||||
|
||||
|
||||
class _MutuallyExclusiveGroup(_ArgumentGroup):
|
||||
|
||||
@@ -1668,6 +1725,14 @@ class _MutuallyExclusiveGroup(_ArgumentGroup):
|
||||
self._container._remove_action(action)
|
||||
self._group_actions.remove(action)
|
||||
|
||||
def add_mutually_exclusive_group(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"Nesting mutually exclusive groups is deprecated.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return super().add_mutually_exclusive_group(*args, **kwargs)
|
||||
|
||||
|
||||
class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
"""Object for parsing command line strings into Python objects.
|
||||
@@ -1857,8 +1922,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
if self.exit_on_error:
|
||||
try:
|
||||
namespace, args = self._parse_known_args(args, namespace)
|
||||
except ArgumentError:
|
||||
err = _sys.exc_info()[1]
|
||||
except ArgumentError as err:
|
||||
self.error(str(err))
|
||||
else:
|
||||
namespace, args = self._parse_known_args(args, namespace)
|
||||
@@ -1962,7 +2026,11 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
# arguments, try to parse more single-dash options out
|
||||
# of the tail of the option string
|
||||
chars = self.prefix_chars
|
||||
if arg_count == 0 and option_string[1] not in chars:
|
||||
if (
|
||||
arg_count == 0
|
||||
and option_string[1] not in chars
|
||||
and explicit_arg != ''
|
||||
):
|
||||
action_tuples.append((action, [], option_string))
|
||||
char = option_string[0]
|
||||
option_string = char + explicit_arg[0]
|
||||
@@ -2126,15 +2194,16 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
# replace arguments referencing files with the file content
|
||||
else:
|
||||
try:
|
||||
with open(arg_string[1:]) as args_file:
|
||||
with open(arg_string[1:],
|
||||
encoding=_sys.getfilesystemencoding(),
|
||||
errors=_sys.getfilesystemencodeerrors()) as args_file:
|
||||
arg_strings = []
|
||||
for arg_line in args_file.read().splitlines():
|
||||
for arg in self.convert_arg_line_to_args(arg_line):
|
||||
arg_strings.append(arg)
|
||||
arg_strings = self._read_args_from_files(arg_strings)
|
||||
new_arg_strings.extend(arg_strings)
|
||||
except OSError:
|
||||
err = _sys.exc_info()[1]
|
||||
except OSError as err:
|
||||
self.error(str(err))
|
||||
|
||||
# return the modified argument list
|
||||
@@ -2441,9 +2510,11 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
not action.option_strings):
|
||||
if action.default is not None:
|
||||
value = action.default
|
||||
self._check_value(action, value)
|
||||
else:
|
||||
# since arg_strings is always [] at this point
|
||||
# there is no need to use self._check_value(action, value)
|
||||
value = arg_strings
|
||||
self._check_value(action, value)
|
||||
|
||||
# single argument or optional argument produces a single value
|
||||
elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]:
|
||||
@@ -2484,9 +2555,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
result = type_func(arg_string)
|
||||
|
||||
# ArgumentTypeErrors indicate errors
|
||||
except ArgumentTypeError:
|
||||
name = getattr(action.type, '__name__', repr(action.type))
|
||||
msg = str(_sys.exc_info()[1])
|
||||
except ArgumentTypeError as err:
|
||||
msg = str(err)
|
||||
raise ArgumentError(action, msg)
|
||||
|
||||
# TypeErrors or ValueErrors also indicate errors
|
||||
@@ -2557,9 +2627,11 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||
|
||||
def _print_message(self, message, file=None):
|
||||
if message:
|
||||
if file is None:
|
||||
file = _sys.stderr
|
||||
file.write(message)
|
||||
file = file or _sys.stderr
|
||||
try:
|
||||
file.write(message)
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
# ===============
|
||||
# Exiting methods
|
||||
|
||||
362
Lib/ast.py
vendored
362
Lib/ast.py
vendored
@@ -25,9 +25,10 @@
|
||||
:license: Python License.
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
from _ast import *
|
||||
from contextlib import contextmanager, nullcontext
|
||||
from enum import IntEnum, auto
|
||||
from enum import IntEnum, auto, _simple_enum
|
||||
|
||||
|
||||
def parse(source, filename='<unknown>', mode='exec', *,
|
||||
@@ -40,12 +41,13 @@ def parse(source, filename='<unknown>', mode='exec', *,
|
||||
flags = PyCF_ONLY_AST
|
||||
if type_comments:
|
||||
flags |= PyCF_TYPE_COMMENTS
|
||||
if isinstance(feature_version, tuple):
|
||||
major, minor = feature_version # Should be a 2-tuple.
|
||||
assert major == 3
|
||||
feature_version = minor
|
||||
elif feature_version is None:
|
||||
if feature_version is None:
|
||||
feature_version = -1
|
||||
elif isinstance(feature_version, tuple):
|
||||
major, minor = feature_version # Should be a 2-tuple.
|
||||
if major != 3:
|
||||
raise ValueError(f"Unsupported major version: {major}")
|
||||
feature_version = minor
|
||||
# Else it should be an int giving the minor version for 3.x.
|
||||
return compile(source, filename, mode, flags,
|
||||
_feature_version=feature_version)
|
||||
@@ -53,10 +55,12 @@ def parse(source, filename='<unknown>', mode='exec', *,
|
||||
|
||||
def literal_eval(node_or_string):
|
||||
"""
|
||||
Safely evaluate an expression node or a string containing a Python
|
||||
Evaluate an expression node or a string containing only a Python
|
||||
expression. The string or node provided may only consist of the following
|
||||
Python literal structures: strings, bytes, numbers, tuples, lists, dicts,
|
||||
sets, booleans, and None.
|
||||
|
||||
Caution: A complex expression can overflow the C stack and cause a crash.
|
||||
"""
|
||||
if isinstance(node_or_string, str):
|
||||
node_or_string = parse(node_or_string.lstrip(" \t"), mode='eval')
|
||||
@@ -234,6 +238,12 @@ def increment_lineno(node, n=1):
|
||||
location in a file.
|
||||
"""
|
||||
for child in walk(node):
|
||||
# TypeIgnore is a special case where lineno is not an attribute
|
||||
# but rather a field of the node itself.
|
||||
if isinstance(child, TypeIgnore):
|
||||
child.lineno = getattr(child, 'lineno', 0) + n
|
||||
continue
|
||||
|
||||
if 'lineno' in child._attributes:
|
||||
child.lineno = getattr(child, 'lineno', 0) + n
|
||||
if (
|
||||
@@ -284,9 +294,7 @@ def get_docstring(node, clean=True):
|
||||
if not(node.body and isinstance(node.body[0], Expr)):
|
||||
return None
|
||||
node = node.body[0].value
|
||||
if isinstance(node, Str):
|
||||
text = node.s
|
||||
elif isinstance(node, Constant) and isinstance(node.value, str):
|
||||
if isinstance(node, Constant) and isinstance(node.value, str):
|
||||
text = node.value
|
||||
else:
|
||||
return None
|
||||
@@ -296,28 +304,17 @@ def get_docstring(node, clean=True):
|
||||
return text
|
||||
|
||||
|
||||
def _splitlines_no_ff(source):
|
||||
_line_pattern = re.compile(r"(.*?(?:\r\n|\n|\r|$))")
|
||||
def _splitlines_no_ff(source, maxlines=None):
|
||||
"""Split a string into lines ignoring form feed and other chars.
|
||||
|
||||
This mimics how the Python parser splits source code.
|
||||
"""
|
||||
idx = 0
|
||||
lines = []
|
||||
next_line = ''
|
||||
while idx < len(source):
|
||||
c = source[idx]
|
||||
next_line += c
|
||||
idx += 1
|
||||
# Keep \r\n together
|
||||
if c == '\r' and idx < len(source) and source[idx] == '\n':
|
||||
next_line += '\n'
|
||||
idx += 1
|
||||
if c in '\r\n':
|
||||
lines.append(next_line)
|
||||
next_line = ''
|
||||
|
||||
if next_line:
|
||||
lines.append(next_line)
|
||||
for lineno, match in enumerate(_line_pattern.finditer(source), 1):
|
||||
if maxlines is not None and lineno > maxlines:
|
||||
break
|
||||
lines.append(match[0])
|
||||
return lines
|
||||
|
||||
|
||||
@@ -351,7 +348,7 @@ def get_source_segment(source, node, *, padded=False):
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
lines = _splitlines_no_ff(source)
|
||||
lines = _splitlines_no_ff(source, maxlines=end_lineno+1)
|
||||
if end_lineno == lineno:
|
||||
return lines[lineno].encode()[col_offset:end_col_offset].decode()
|
||||
|
||||
@@ -500,20 +497,52 @@ class NodeTransformer(NodeVisitor):
|
||||
return node
|
||||
|
||||
|
||||
_DEPRECATED_VALUE_ALIAS_MESSAGE = (
|
||||
"{name} is deprecated and will be removed in Python {remove}; use value instead"
|
||||
)
|
||||
_DEPRECATED_CLASS_MESSAGE = (
|
||||
"{name} is deprecated and will be removed in Python {remove}; "
|
||||
"use ast.Constant instead"
|
||||
)
|
||||
|
||||
|
||||
# If the ast module is loaded more than once, only add deprecated methods once
|
||||
if not hasattr(Constant, 'n'):
|
||||
# The following code is for backward compatibility.
|
||||
# It will be removed in future.
|
||||
|
||||
def _getter(self):
|
||||
def _n_getter(self):
|
||||
"""Deprecated. Use value instead."""
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
"Attribute n", message=_DEPRECATED_VALUE_ALIAS_MESSAGE, remove=(3, 14)
|
||||
)
|
||||
return self.value
|
||||
|
||||
def _setter(self, value):
|
||||
def _n_setter(self, value):
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
"Attribute n", message=_DEPRECATED_VALUE_ALIAS_MESSAGE, remove=(3, 14)
|
||||
)
|
||||
self.value = value
|
||||
|
||||
Constant.n = property(_getter, _setter)
|
||||
Constant.s = property(_getter, _setter)
|
||||
def _s_getter(self):
|
||||
"""Deprecated. Use value instead."""
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
"Attribute s", message=_DEPRECATED_VALUE_ALIAS_MESSAGE, remove=(3, 14)
|
||||
)
|
||||
return self.value
|
||||
|
||||
def _s_setter(self, value):
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
"Attribute s", message=_DEPRECATED_VALUE_ALIAS_MESSAGE, remove=(3, 14)
|
||||
)
|
||||
self.value = value
|
||||
|
||||
Constant.n = property(_n_getter, _n_setter)
|
||||
Constant.s = property(_s_getter, _s_setter)
|
||||
|
||||
class _ABC(type):
|
||||
|
||||
@@ -521,6 +550,13 @@ class _ABC(type):
|
||||
cls.__doc__ = """Deprecated AST node class. Use ast.Constant instead"""
|
||||
|
||||
def __instancecheck__(cls, inst):
|
||||
if cls in _const_types:
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
f"ast.{cls.__qualname__}",
|
||||
message=_DEPRECATED_CLASS_MESSAGE,
|
||||
remove=(3, 14)
|
||||
)
|
||||
if not isinstance(inst, Constant):
|
||||
return False
|
||||
if cls in _const_types:
|
||||
@@ -544,6 +580,10 @@ def _new(cls, *args, **kwargs):
|
||||
if pos < len(args):
|
||||
raise TypeError(f"{cls.__name__} got multiple values for argument {key!r}")
|
||||
if cls in _const_types:
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
f"ast.{cls.__qualname__}", message=_DEPRECATED_CLASS_MESSAGE, remove=(3, 14)
|
||||
)
|
||||
return Constant(*args, **kwargs)
|
||||
return Constant.__new__(cls, *args, **kwargs)
|
||||
|
||||
@@ -566,10 +606,19 @@ class Ellipsis(Constant, metaclass=_ABC):
|
||||
_fields = ()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is Ellipsis:
|
||||
if cls is _ast_Ellipsis:
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
"ast.Ellipsis", message=_DEPRECATED_CLASS_MESSAGE, remove=(3, 14)
|
||||
)
|
||||
return Constant(..., *args, **kwargs)
|
||||
return Constant.__new__(cls, *args, **kwargs)
|
||||
|
||||
# Keep another reference to Ellipsis in the global namespace
|
||||
# so it can be referenced in Ellipsis.__new__
|
||||
# (The original "Ellipsis" name is removed from the global namespace later on)
|
||||
_ast_Ellipsis = Ellipsis
|
||||
|
||||
_const_types = {
|
||||
Num: (int, float, complex),
|
||||
Str: (str,),
|
||||
@@ -636,10 +685,12 @@ class Param(expr_context):
|
||||
# We unparse those infinities to INFSTR.
|
||||
_INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1)
|
||||
|
||||
class _Precedence(IntEnum):
|
||||
@_simple_enum(IntEnum)
|
||||
class _Precedence:
|
||||
"""Precedence table that originated from python grammar."""
|
||||
|
||||
TUPLE = auto()
|
||||
NAMED_EXPR = auto() # <target> := <expr1>
|
||||
TUPLE = auto() # <expr1>, <expr2>
|
||||
YIELD = auto() # 'yield', 'yield from'
|
||||
TEST = auto() # 'if'-'else', 'lambda'
|
||||
OR = auto() # 'or'
|
||||
@@ -677,11 +728,11 @@ class _Unparser(NodeVisitor):
|
||||
|
||||
def __init__(self, *, _avoid_backslashes=False):
|
||||
self._source = []
|
||||
self._buffer = []
|
||||
self._precedences = {}
|
||||
self._type_ignores = {}
|
||||
self._indent = 0
|
||||
self._avoid_backslashes = _avoid_backslashes
|
||||
self._in_try_star = False
|
||||
|
||||
def interleave(self, inter, f, seq):
|
||||
"""Call f on each item in seq, calling inter() in between."""
|
||||
@@ -716,18 +767,19 @@ class _Unparser(NodeVisitor):
|
||||
self.maybe_newline()
|
||||
self.write(" " * self._indent + text)
|
||||
|
||||
def write(self, text):
|
||||
"""Append a piece of text"""
|
||||
self._source.append(text)
|
||||
def write(self, *text):
|
||||
"""Add new source parts"""
|
||||
self._source.extend(text)
|
||||
|
||||
def buffer_writer(self, text):
|
||||
self._buffer.append(text)
|
||||
@contextmanager
|
||||
def buffered(self, buffer = None):
|
||||
if buffer is None:
|
||||
buffer = []
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
value = "".join(self._buffer)
|
||||
self._buffer.clear()
|
||||
return value
|
||||
original_source = self._source
|
||||
self._source = buffer
|
||||
yield buffer
|
||||
self._source = original_source
|
||||
|
||||
@contextmanager
|
||||
def block(self, *, extra = None):
|
||||
@@ -837,7 +889,7 @@ class _Unparser(NodeVisitor):
|
||||
self.traverse(node.value)
|
||||
|
||||
def visit_NamedExpr(self, node):
|
||||
with self.require_parens(_Precedence.TUPLE, node):
|
||||
with self.require_parens(_Precedence.NAMED_EXPR, node):
|
||||
self.set_precedence(_Precedence.ATOM, node.target, node.value)
|
||||
self.traverse(node.target)
|
||||
self.write(" := ")
|
||||
@@ -849,7 +901,7 @@ class _Unparser(NodeVisitor):
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
self.fill("from ")
|
||||
self.write("." * node.level)
|
||||
self.write("." * (node.level or 0))
|
||||
if node.module:
|
||||
self.write(node.module)
|
||||
self.write(" import ")
|
||||
@@ -858,6 +910,7 @@ class _Unparser(NodeVisitor):
|
||||
def visit_Assign(self, node):
|
||||
self.fill()
|
||||
for target in node.targets:
|
||||
self.set_precedence(_Precedence.TUPLE, target)
|
||||
self.traverse(target)
|
||||
self.write(" = ")
|
||||
self.traverse(node.value)
|
||||
@@ -950,7 +1003,7 @@ class _Unparser(NodeVisitor):
|
||||
self.write(" from ")
|
||||
self.traverse(node.cause)
|
||||
|
||||
def visit_Try(self, node):
|
||||
def do_visit_try(self, node):
|
||||
self.fill("try")
|
||||
with self.block():
|
||||
self.traverse(node.body)
|
||||
@@ -965,8 +1018,24 @@ class _Unparser(NodeVisitor):
|
||||
with self.block():
|
||||
self.traverse(node.finalbody)
|
||||
|
||||
def visit_Try(self, node):
|
||||
prev_in_try_star = self._in_try_star
|
||||
try:
|
||||
self._in_try_star = False
|
||||
self.do_visit_try(node)
|
||||
finally:
|
||||
self._in_try_star = prev_in_try_star
|
||||
|
||||
def visit_TryStar(self, node):
|
||||
prev_in_try_star = self._in_try_star
|
||||
try:
|
||||
self._in_try_star = True
|
||||
self.do_visit_try(node)
|
||||
finally:
|
||||
self._in_try_star = prev_in_try_star
|
||||
|
||||
def visit_ExceptHandler(self, node):
|
||||
self.fill("except")
|
||||
self.fill("except*" if self._in_try_star else "except")
|
||||
if node.type:
|
||||
self.write(" ")
|
||||
self.traverse(node.type)
|
||||
@@ -982,6 +1051,8 @@ class _Unparser(NodeVisitor):
|
||||
self.fill("@")
|
||||
self.traverse(deco)
|
||||
self.fill("class " + node.name)
|
||||
if hasattr(node, "type_params"):
|
||||
self._type_params_helper(node.type_params)
|
||||
with self.delimit_if("(", ")", condition = node.bases or node.keywords):
|
||||
comma = False
|
||||
for e in node.bases:
|
||||
@@ -1013,6 +1084,8 @@ class _Unparser(NodeVisitor):
|
||||
self.traverse(deco)
|
||||
def_str = fill_suffix + " " + node.name
|
||||
self.fill(def_str)
|
||||
if hasattr(node, "type_params"):
|
||||
self._type_params_helper(node.type_params)
|
||||
with self.delimit("(", ")"):
|
||||
self.traverse(node.args)
|
||||
if node.returns:
|
||||
@@ -1021,6 +1094,30 @@ class _Unparser(NodeVisitor):
|
||||
with self.block(extra=self.get_type_comment(node)):
|
||||
self._write_docstring_and_traverse_body(node)
|
||||
|
||||
def _type_params_helper(self, type_params):
|
||||
if type_params is not None and len(type_params) > 0:
|
||||
with self.delimit("[", "]"):
|
||||
self.interleave(lambda: self.write(", "), self.traverse, type_params)
|
||||
|
||||
def visit_TypeVar(self, node):
|
||||
self.write(node.name)
|
||||
if node.bound:
|
||||
self.write(": ")
|
||||
self.traverse(node.bound)
|
||||
|
||||
def visit_TypeVarTuple(self, node):
|
||||
self.write("*" + node.name)
|
||||
|
||||
def visit_ParamSpec(self, node):
|
||||
self.write("**" + node.name)
|
||||
|
||||
def visit_TypeAlias(self, node):
|
||||
self.fill("type ")
|
||||
self.traverse(node.name)
|
||||
self._type_params_helper(node.type_params)
|
||||
self.write(" = ")
|
||||
self.traverse(node.value)
|
||||
|
||||
def visit_For(self, node):
|
||||
self._for_helper("for ", node)
|
||||
|
||||
@@ -1029,6 +1126,7 @@ class _Unparser(NodeVisitor):
|
||||
|
||||
def _for_helper(self, fill, node):
|
||||
self.fill(fill)
|
||||
self.set_precedence(_Precedence.TUPLE, node.target)
|
||||
self.traverse(node.target)
|
||||
self.write(" in ")
|
||||
self.traverse(node.iter)
|
||||
@@ -1125,71 +1223,81 @@ class _Unparser(NodeVisitor):
|
||||
|
||||
def visit_JoinedStr(self, node):
|
||||
self.write("f")
|
||||
if self._avoid_backslashes:
|
||||
self._fstring_JoinedStr(node, self.buffer_writer)
|
||||
self._write_str_avoiding_backslashes(self.buffer)
|
||||
return
|
||||
|
||||
# If we don't need to avoid backslashes globally (i.e., we only need
|
||||
# to avoid them inside FormattedValues), it's cosmetically preferred
|
||||
# to use escaped whitespace. That is, it's preferred to use backslashes
|
||||
# for cases like: f"{x}\n". To accomplish this, we keep track of what
|
||||
# in our buffer corresponds to FormattedValues and what corresponds to
|
||||
# Constant parts of the f-string, and allow escapes accordingly.
|
||||
buffer = []
|
||||
fstring_parts = []
|
||||
for value in node.values:
|
||||
meth = getattr(self, "_fstring_" + type(value).__name__)
|
||||
meth(value, self.buffer_writer)
|
||||
buffer.append((self.buffer, isinstance(value, Constant)))
|
||||
new_buffer = []
|
||||
quote_types = _ALL_QUOTES
|
||||
for value, is_constant in buffer:
|
||||
# Repeatedly narrow down the list of possible quote_types
|
||||
value, quote_types = self._str_literal_helper(
|
||||
value, quote_types=quote_types,
|
||||
escape_special_whitespace=is_constant
|
||||
with self.buffered() as buffer:
|
||||
self._write_fstring_inner(value)
|
||||
fstring_parts.append(
|
||||
("".join(buffer), isinstance(value, Constant))
|
||||
)
|
||||
new_buffer.append(value)
|
||||
value = "".join(new_buffer)
|
||||
|
||||
new_fstring_parts = []
|
||||
quote_types = list(_ALL_QUOTES)
|
||||
fallback_to_repr = False
|
||||
for value, is_constant in fstring_parts:
|
||||
if is_constant:
|
||||
value, new_quote_types = self._str_literal_helper(
|
||||
value,
|
||||
quote_types=quote_types,
|
||||
escape_special_whitespace=True,
|
||||
)
|
||||
if set(new_quote_types).isdisjoint(quote_types):
|
||||
fallback_to_repr = True
|
||||
break
|
||||
quote_types = new_quote_types
|
||||
elif "\n" in value:
|
||||
quote_types = [q for q in quote_types if q in _MULTI_QUOTES]
|
||||
assert quote_types
|
||||
new_fstring_parts.append(value)
|
||||
|
||||
if fallback_to_repr:
|
||||
# If we weren't able to find a quote type that works for all parts
|
||||
# of the JoinedStr, fallback to using repr and triple single quotes.
|
||||
quote_types = ["'''"]
|
||||
new_fstring_parts.clear()
|
||||
for value, is_constant in fstring_parts:
|
||||
if is_constant:
|
||||
value = repr('"' + value) # force repr to use single quotes
|
||||
expected_prefix = "'\""
|
||||
assert value.startswith(expected_prefix), repr(value)
|
||||
value = value[len(expected_prefix):-1]
|
||||
new_fstring_parts.append(value)
|
||||
|
||||
value = "".join(new_fstring_parts)
|
||||
quote_type = quote_types[0]
|
||||
self.write(f"{quote_type}{value}{quote_type}")
|
||||
|
||||
def _write_fstring_inner(self, node):
|
||||
if isinstance(node, JoinedStr):
|
||||
# for both the f-string itself, and format_spec
|
||||
for value in node.values:
|
||||
self._write_fstring_inner(value)
|
||||
elif isinstance(node, Constant) and isinstance(node.value, str):
|
||||
value = node.value.replace("{", "{{").replace("}", "}}")
|
||||
self.write(value)
|
||||
elif isinstance(node, FormattedValue):
|
||||
self.visit_FormattedValue(node)
|
||||
else:
|
||||
raise ValueError(f"Unexpected node inside JoinedStr, {node!r}")
|
||||
|
||||
def visit_FormattedValue(self, node):
|
||||
self.write("f")
|
||||
self._fstring_FormattedValue(node, self.buffer_writer)
|
||||
self._write_str_avoiding_backslashes(self.buffer)
|
||||
def unparse_inner(inner):
|
||||
unparser = type(self)()
|
||||
unparser.set_precedence(_Precedence.TEST.next(), inner)
|
||||
return unparser.visit(inner)
|
||||
|
||||
def _fstring_JoinedStr(self, node, write):
|
||||
for value in node.values:
|
||||
meth = getattr(self, "_fstring_" + type(value).__name__)
|
||||
meth(value, write)
|
||||
|
||||
def _fstring_Constant(self, node, write):
|
||||
if not isinstance(node.value, str):
|
||||
raise ValueError("Constants inside JoinedStr should be a string.")
|
||||
value = node.value.replace("{", "{{").replace("}", "}}")
|
||||
write(value)
|
||||
|
||||
def _fstring_FormattedValue(self, node, write):
|
||||
write("{")
|
||||
unparser = type(self)(_avoid_backslashes=True)
|
||||
unparser.set_precedence(_Precedence.TEST.next(), node.value)
|
||||
expr = unparser.visit(node.value)
|
||||
if expr.startswith("{"):
|
||||
write(" ") # Separate pair of opening brackets as "{ {"
|
||||
if "\\" in expr:
|
||||
raise ValueError("Unable to avoid backslash in f-string expression part")
|
||||
write(expr)
|
||||
if node.conversion != -1:
|
||||
conversion = chr(node.conversion)
|
||||
if conversion not in "sra":
|
||||
raise ValueError("Unknown f-string conversion.")
|
||||
write(f"!{conversion}")
|
||||
if node.format_spec:
|
||||
write(":")
|
||||
meth = getattr(self, "_fstring_" + type(node.format_spec).__name__)
|
||||
meth(node.format_spec, write)
|
||||
write("}")
|
||||
with self.delimit("{", "}"):
|
||||
expr = unparse_inner(node.value)
|
||||
if expr.startswith("{"):
|
||||
# Separate pair of opening brackets as "{ {"
|
||||
self.write(" ")
|
||||
self.write(expr)
|
||||
if node.conversion != -1:
|
||||
self.write(f"!{chr(node.conversion)}")
|
||||
if node.format_spec:
|
||||
self.write(":")
|
||||
self._write_fstring_inner(node.format_spec)
|
||||
|
||||
def visit_Name(self, node):
|
||||
self.write(node.id)
|
||||
@@ -1312,7 +1420,11 @@ class _Unparser(NodeVisitor):
|
||||
)
|
||||
|
||||
def visit_Tuple(self, node):
|
||||
with self.delimit("(", ")"):
|
||||
with self.delimit_if(
|
||||
"(",
|
||||
")",
|
||||
len(node.elts) == 0 or self.get_precedence(node) > _Precedence.TUPLE
|
||||
):
|
||||
self.items_view(self.traverse, node.elts)
|
||||
|
||||
unop = {"Invert": "~", "Not": "not", "UAdd": "+", "USub": "-"}
|
||||
@@ -1328,7 +1440,7 @@ class _Unparser(NodeVisitor):
|
||||
operator_precedence = self.unop_precedence[operator]
|
||||
with self.require_parens(operator_precedence, node):
|
||||
self.write(operator)
|
||||
# factor prefixes (+, -, ~) shouldn't be seperated
|
||||
# factor prefixes (+, -, ~) shouldn't be separated
|
||||
# from the value they belong, (e.g: +1 instead of + 1)
|
||||
if operator_precedence is not _Precedence.FACTOR:
|
||||
self.write(" ")
|
||||
@@ -1453,20 +1565,17 @@ class _Unparser(NodeVisitor):
|
||||
self.traverse(e)
|
||||
|
||||
def visit_Subscript(self, node):
|
||||
def is_simple_tuple(slice_value):
|
||||
# when unparsing a non-empty tuple, the parentheses can be safely
|
||||
# omitted if there aren't any elements that explicitly requires
|
||||
# parentheses (such as starred expressions).
|
||||
def is_non_empty_tuple(slice_value):
|
||||
return (
|
||||
isinstance(slice_value, Tuple)
|
||||
and slice_value.elts
|
||||
and not any(isinstance(elt, Starred) for elt in slice_value.elts)
|
||||
)
|
||||
|
||||
self.set_precedence(_Precedence.ATOM, node.value)
|
||||
self.traverse(node.value)
|
||||
with self.delimit("[", "]"):
|
||||
if is_simple_tuple(node.slice):
|
||||
if is_non_empty_tuple(node.slice):
|
||||
# parentheses can be omitted if the tuple isn't empty
|
||||
self.items_view(self.traverse, node.slice.elts)
|
||||
else:
|
||||
self.traverse(node.slice)
|
||||
@@ -1563,8 +1672,11 @@ class _Unparser(NodeVisitor):
|
||||
|
||||
def visit_Lambda(self, node):
|
||||
with self.require_parens(_Precedence.TEST, node):
|
||||
self.write("lambda ")
|
||||
self.traverse(node.args)
|
||||
self.write("lambda")
|
||||
with self.buffered() as buffer:
|
||||
self.traverse(node.args)
|
||||
if buffer:
|
||||
self.write(" ", *buffer)
|
||||
self.write(": ")
|
||||
self.set_precedence(_Precedence.TEST, node.body)
|
||||
self.traverse(node.body)
|
||||
@@ -1673,6 +1785,22 @@ def unparse(ast_obj):
|
||||
return unparser.visit(ast_obj)
|
||||
|
||||
|
||||
_deprecated_globals = {
|
||||
name: globals().pop(name)
|
||||
for name in ('Num', 'Str', 'Bytes', 'NameConstant', 'Ellipsis')
|
||||
}
|
||||
|
||||
def __getattr__(name):
|
||||
if name in _deprecated_globals:
|
||||
globals()[name] = value = _deprecated_globals[name]
|
||||
import warnings
|
||||
warnings._deprecated(
|
||||
f"ast.{name}", message=_DEPRECATED_CLASS_MESSAGE, remove=(3, 14)
|
||||
)
|
||||
return value
|
||||
raise AttributeError(f"module 'ast' has no attribute '{name}'")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
|
||||
19
Lib/asyncio/__init__.py
vendored
19
Lib/asyncio/__init__.py
vendored
@@ -1,22 +1,14 @@
|
||||
"""The asyncio package, tracking PEP 3156."""
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
import sys
|
||||
|
||||
import selectors
|
||||
# XXX RustPython TODO: _overlapped
|
||||
if sys.platform == 'win32' and False:
|
||||
# Similar thing for _overlapped.
|
||||
try:
|
||||
from . import _overlapped
|
||||
except ImportError:
|
||||
import _overlapped # Will also be exported.
|
||||
|
||||
|
||||
# This relies on each of the submodules having an __all__ variable.
|
||||
from .base_events import *
|
||||
from .coroutines import *
|
||||
from .events import *
|
||||
from .exceptions import *
|
||||
from .futures import *
|
||||
from .locks import *
|
||||
from .protocols import *
|
||||
@@ -25,11 +17,15 @@ from .queues import *
|
||||
from .streams import *
|
||||
from .subprocess import *
|
||||
from .tasks import *
|
||||
from .taskgroups import *
|
||||
from .timeouts import *
|
||||
from .threads import *
|
||||
from .transports import *
|
||||
|
||||
__all__ = (base_events.__all__ +
|
||||
coroutines.__all__ +
|
||||
events.__all__ +
|
||||
exceptions.__all__ +
|
||||
futures.__all__ +
|
||||
locks.__all__ +
|
||||
protocols.__all__ +
|
||||
@@ -38,6 +34,9 @@ __all__ = (base_events.__all__ +
|
||||
streams.__all__ +
|
||||
subprocess.__all__ +
|
||||
tasks.__all__ +
|
||||
taskgroups.__all__ +
|
||||
threads.__all__ +
|
||||
timeouts.__all__ +
|
||||
transports.__all__)
|
||||
|
||||
if sys.platform == 'win32': # pragma: no cover
|
||||
|
||||
125
Lib/asyncio/__main__.py
vendored
Normal file
125
Lib/asyncio/__main__.py
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
import ast
|
||||
import asyncio
|
||||
import code
|
||||
import concurrent.futures
|
||||
import inspect
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
import warnings
|
||||
|
||||
from . import futures
|
||||
|
||||
|
||||
class AsyncIOInteractiveConsole(code.InteractiveConsole):
|
||||
|
||||
def __init__(self, locals, loop):
|
||||
super().__init__(locals)
|
||||
self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
|
||||
|
||||
self.loop = loop
|
||||
|
||||
def runcode(self, code):
|
||||
future = concurrent.futures.Future()
|
||||
|
||||
def callback():
|
||||
global repl_future
|
||||
global repl_future_interrupted
|
||||
|
||||
repl_future = None
|
||||
repl_future_interrupted = False
|
||||
|
||||
func = types.FunctionType(code, self.locals)
|
||||
try:
|
||||
coro = func()
|
||||
except SystemExit:
|
||||
raise
|
||||
except KeyboardInterrupt as ex:
|
||||
repl_future_interrupted = True
|
||||
future.set_exception(ex)
|
||||
return
|
||||
except BaseException as ex:
|
||||
future.set_exception(ex)
|
||||
return
|
||||
|
||||
if not inspect.iscoroutine(coro):
|
||||
future.set_result(coro)
|
||||
return
|
||||
|
||||
try:
|
||||
repl_future = self.loop.create_task(coro)
|
||||
futures._chain_future(repl_future, future)
|
||||
except BaseException as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
loop.call_soon_threadsafe(callback)
|
||||
|
||||
try:
|
||||
return future.result()
|
||||
except SystemExit:
|
||||
raise
|
||||
except BaseException:
|
||||
if repl_future_interrupted:
|
||||
self.write("\nKeyboardInterrupt\n")
|
||||
else:
|
||||
self.showtraceback()
|
||||
|
||||
|
||||
class REPLThread(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
banner = (
|
||||
f'asyncio REPL {sys.version} on {sys.platform}\n'
|
||||
f'Use "await" directly instead of "asyncio.run()".\n'
|
||||
f'Type "help", "copyright", "credits" or "license" '
|
||||
f'for more information.\n'
|
||||
f'{getattr(sys, "ps1", ">>> ")}import asyncio'
|
||||
)
|
||||
|
||||
console.interact(
|
||||
banner=banner,
|
||||
exitmsg='exiting asyncio REPL...')
|
||||
finally:
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
message=r'^coroutine .* was never awaited$',
|
||||
category=RuntimeWarning)
|
||||
|
||||
loop.call_soon_threadsafe(loop.stop)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
repl_locals = {'asyncio': asyncio}
|
||||
for key in {'__name__', '__package__',
|
||||
'__loader__', '__spec__',
|
||||
'__builtins__', '__file__'}:
|
||||
repl_locals[key] = locals()[key]
|
||||
|
||||
console = AsyncIOInteractiveConsole(repl_locals, loop)
|
||||
|
||||
repl_future = None
|
||||
repl_future_interrupted = False
|
||||
|
||||
try:
|
||||
import readline # NoQA
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
repl_thread = REPLThread()
|
||||
repl_thread.daemon = True
|
||||
repl_thread.start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
if repl_future and not repl_future.done():
|
||||
repl_future.cancel()
|
||||
repl_future_interrupted = True
|
||||
continue
|
||||
else:
|
||||
break
|
||||
1356
Lib/asyncio/base_events.py
vendored
1356
Lib/asyncio/base_events.py
vendored
File diff suppressed because it is too large
Load Diff
38
Lib/asyncio/base_futures.py
vendored
38
Lib/asyncio/base_futures.py
vendored
@@ -1,18 +1,8 @@
|
||||
__all__ = []
|
||||
__all__ = ()
|
||||
|
||||
import concurrent.futures._base
|
||||
import reprlib
|
||||
|
||||
from . import events
|
||||
|
||||
Error = concurrent.futures._base.Error
|
||||
CancelledError = concurrent.futures.CancelledError
|
||||
TimeoutError = concurrent.futures.TimeoutError
|
||||
|
||||
|
||||
class InvalidStateError(Error):
|
||||
"""The operation is not allowed in this state."""
|
||||
|
||||
from . import format_helpers
|
||||
|
||||
# States for Future.
|
||||
_PENDING = 'PENDING'
|
||||
@@ -38,17 +28,17 @@ def _format_callbacks(cb):
|
||||
cb = ''
|
||||
|
||||
def format_cb(callback):
|
||||
return events._format_callback_source(callback, ())
|
||||
return format_helpers._format_callback_source(callback, ())
|
||||
|
||||
if size == 1:
|
||||
cb = format_cb(cb[0])
|
||||
cb = format_cb(cb[0][0])
|
||||
elif size == 2:
|
||||
cb = '{}, {}'.format(format_cb(cb[0]), format_cb(cb[1]))
|
||||
cb = '{}, {}'.format(format_cb(cb[0][0]), format_cb(cb[1][0]))
|
||||
elif size > 2:
|
||||
cb = '{}, <{} more>, {}'.format(format_cb(cb[0]),
|
||||
cb = '{}, <{} more>, {}'.format(format_cb(cb[0][0]),
|
||||
size - 2,
|
||||
format_cb(cb[-1]))
|
||||
return 'cb=[%s]' % cb
|
||||
format_cb(cb[-1][0]))
|
||||
return f'cb=[{cb}]'
|
||||
|
||||
|
||||
def _future_repr_info(future):
|
||||
@@ -57,15 +47,21 @@ def _future_repr_info(future):
|
||||
info = [future._state.lower()]
|
||||
if future._state == _FINISHED:
|
||||
if future._exception is not None:
|
||||
info.append('exception={!r}'.format(future._exception))
|
||||
info.append(f'exception={future._exception!r}')
|
||||
else:
|
||||
# use reprlib to limit the length of the output, especially
|
||||
# for very long strings
|
||||
result = reprlib.repr(future._result)
|
||||
info.append('result={}'.format(result))
|
||||
info.append(f'result={result}')
|
||||
if future._callbacks:
|
||||
info.append(_format_callbacks(future._callbacks))
|
||||
if future._source_traceback:
|
||||
frame = future._source_traceback[-1]
|
||||
info.append('created at %s:%s' % (frame[0], frame[1]))
|
||||
info.append(f'created at {frame[0]}:{frame[1]}')
|
||||
return info
|
||||
|
||||
|
||||
@reprlib.recursive_repr()
|
||||
def _future_repr(future):
|
||||
info = ' '.join(_future_repr_info(future))
|
||||
return f'<{future.__class__.__name__} {info}>'
|
||||
|
||||
78
Lib/asyncio/base_subprocess.py
vendored
78
Lib/asyncio/base_subprocess.py
vendored
@@ -2,10 +2,8 @@ import collections
|
||||
import subprocess
|
||||
import warnings
|
||||
|
||||
from . import compat
|
||||
from . import protocols
|
||||
from . import transports
|
||||
from .coroutines import coroutine
|
||||
from .log import logger
|
||||
|
||||
|
||||
@@ -59,9 +57,9 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
if self._closed:
|
||||
info.append('closed')
|
||||
if self._pid is not None:
|
||||
info.append('pid=%s' % self._pid)
|
||||
info.append(f'pid={self._pid}')
|
||||
if self._returncode is not None:
|
||||
info.append('returncode=%s' % self._returncode)
|
||||
info.append(f'returncode={self._returncode}')
|
||||
elif self._pid is not None:
|
||||
info.append('running')
|
||||
else:
|
||||
@@ -69,19 +67,19 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
|
||||
stdin = self._pipes.get(0)
|
||||
if stdin is not None:
|
||||
info.append('stdin=%s' % stdin.pipe)
|
||||
info.append(f'stdin={stdin.pipe}')
|
||||
|
||||
stdout = self._pipes.get(1)
|
||||
stderr = self._pipes.get(2)
|
||||
if stdout is not None and stderr is stdout:
|
||||
info.append('stdout=stderr=%s' % stdout.pipe)
|
||||
info.append(f'stdout=stderr={stdout.pipe}')
|
||||
else:
|
||||
if stdout is not None:
|
||||
info.append('stdout=%s' % stdout.pipe)
|
||||
info.append(f'stdout={stdout.pipe}')
|
||||
if stderr is not None:
|
||||
info.append('stderr=%s' % stderr.pipe)
|
||||
info.append(f'stderr={stderr.pipe}')
|
||||
|
||||
return '<%s>' % ' '.join(info)
|
||||
return '<{}>'.format(' '.join(info))
|
||||
|
||||
def _start(self, args, shell, stdin, stdout, stderr, bufsize, **kwargs):
|
||||
raise NotImplementedError
|
||||
@@ -105,12 +103,13 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
continue
|
||||
proto.pipe.close()
|
||||
|
||||
if (self._proc is not None
|
||||
# the child process finished?
|
||||
and self._returncode is None
|
||||
# the child process finished but the transport was not notified yet?
|
||||
and self._proc.poll() is None
|
||||
):
|
||||
if (self._proc is not None and
|
||||
# has the child process finished?
|
||||
self._returncode is None and
|
||||
# the child process has finished, but the
|
||||
# transport hasn't been notified yet?
|
||||
self._proc.poll() is None):
|
||||
|
||||
if self._loop.get_debug():
|
||||
logger.warning('Close running child process: kill %r', self)
|
||||
|
||||
@@ -121,15 +120,10 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
|
||||
# Don't clear the _proc reference yet: _post_init() may still run
|
||||
|
||||
# On Python 3.3 and older, objects with a destructor part of a reference
|
||||
# cycle are never destroyed. It's not more the case on Python 3.4 thanks
|
||||
# to the PEP 442.
|
||||
if compat.PY34:
|
||||
def __del__(self):
|
||||
if not self._closed:
|
||||
warnings.warn("unclosed transport %r" % self, ResourceWarning,
|
||||
source=self)
|
||||
self.close()
|
||||
def __del__(self, _warn=warnings.warn):
|
||||
if not self._closed:
|
||||
_warn(f"unclosed transport {self!r}", ResourceWarning, source=self)
|
||||
self.close()
|
||||
|
||||
def get_pid(self):
|
||||
return self._pid
|
||||
@@ -159,26 +153,25 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
self._check_proc()
|
||||
self._proc.kill()
|
||||
|
||||
@coroutine
|
||||
def _connect_pipes(self, waiter):
|
||||
async def _connect_pipes(self, waiter):
|
||||
try:
|
||||
proc = self._proc
|
||||
loop = self._loop
|
||||
|
||||
if proc.stdin is not None:
|
||||
_, pipe = yield from loop.connect_write_pipe(
|
||||
_, pipe = await loop.connect_write_pipe(
|
||||
lambda: WriteSubprocessPipeProto(self, 0),
|
||||
proc.stdin)
|
||||
self._pipes[0] = pipe
|
||||
|
||||
if proc.stdout is not None:
|
||||
_, pipe = yield from loop.connect_read_pipe(
|
||||
_, pipe = await loop.connect_read_pipe(
|
||||
lambda: ReadSubprocessPipeProto(self, 1),
|
||||
proc.stdout)
|
||||
self._pipes[1] = pipe
|
||||
|
||||
if proc.stderr is not None:
|
||||
_, pipe = yield from loop.connect_read_pipe(
|
||||
_, pipe = await loop.connect_read_pipe(
|
||||
lambda: ReadSubprocessPipeProto(self, 2),
|
||||
proc.stderr)
|
||||
self._pipes[2] = pipe
|
||||
@@ -189,7 +182,9 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
for callback, data in self._pending_calls:
|
||||
loop.call_soon(callback, *data)
|
||||
self._pending_calls = None
|
||||
except Exception as exc:
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
if waiter is not None and not waiter.cancelled():
|
||||
waiter.set_exception(exc)
|
||||
else:
|
||||
@@ -213,24 +208,17 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
assert returncode is not None, returncode
|
||||
assert self._returncode is None, self._returncode
|
||||
if self._loop.get_debug():
|
||||
logger.info('%r exited with return code %r',
|
||||
self, returncode)
|
||||
logger.info('%r exited with return code %r', self, returncode)
|
||||
self._returncode = returncode
|
||||
if self._proc.returncode is None:
|
||||
# asyncio uses a child watcher: copy the status into the Popen
|
||||
# object. On Python 3.6, it is required to avoid a ResourceWarning.
|
||||
self._proc.returncode = returncode
|
||||
self._call(self._protocol.process_exited)
|
||||
|
||||
self._try_finish()
|
||||
|
||||
# wake up futures waiting for wait()
|
||||
for waiter in self._exit_waiters:
|
||||
if not waiter.cancelled():
|
||||
waiter.set_result(returncode)
|
||||
self._exit_waiters = None
|
||||
|
||||
@coroutine
|
||||
def _wait(self):
|
||||
async def _wait(self):
|
||||
"""Wait until the process exit and return the process return code.
|
||||
|
||||
This method is a coroutine."""
|
||||
@@ -239,7 +227,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
|
||||
waiter = self._loop.create_future()
|
||||
self._exit_waiters.append(waiter)
|
||||
return (yield from waiter)
|
||||
return await waiter
|
||||
|
||||
def _try_finish(self):
|
||||
assert not self._finished
|
||||
@@ -254,6 +242,11 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
try:
|
||||
self._protocol.connection_lost(exc)
|
||||
finally:
|
||||
# wake up futures waiting for wait()
|
||||
for waiter in self._exit_waiters:
|
||||
if not waiter.cancelled():
|
||||
waiter.set_result(self._returncode)
|
||||
self._exit_waiters = None
|
||||
self._loop = None
|
||||
self._proc = None
|
||||
self._protocol = None
|
||||
@@ -271,8 +264,7 @@ class WriteSubprocessPipeProto(protocols.BaseProtocol):
|
||||
self.pipe = transport
|
||||
|
||||
def __repr__(self):
|
||||
return ('<%s fd=%s pipe=%r>'
|
||||
% (self.__class__.__name__, self.fd, self.pipe))
|
||||
return f'<{self.__class__.__name__} fd={self.fd} pipe={self.pipe!r}>'
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.disconnected = True
|
||||
|
||||
42
Lib/asyncio/base_tasks.py
vendored
42
Lib/asyncio/base_tasks.py
vendored
@@ -1,4 +1,5 @@
|
||||
import linecache
|
||||
import reprlib
|
||||
import traceback
|
||||
|
||||
from . import base_futures
|
||||
@@ -8,25 +9,42 @@ from . import coroutines
|
||||
def _task_repr_info(task):
|
||||
info = base_futures._future_repr_info(task)
|
||||
|
||||
if task._must_cancel:
|
||||
if task.cancelling() and not task.done():
|
||||
# replace status
|
||||
info[0] = 'cancelling'
|
||||
|
||||
coro = coroutines._format_coroutine(task._coro)
|
||||
info.insert(1, 'coro=<%s>' % coro)
|
||||
info.insert(1, 'name=%r' % task.get_name())
|
||||
|
||||
if task._fut_waiter is not None:
|
||||
info.insert(2, 'wait_for=%r' % task._fut_waiter)
|
||||
info.insert(2, f'wait_for={task._fut_waiter!r}')
|
||||
|
||||
if task._coro:
|
||||
coro = coroutines._format_coroutine(task._coro)
|
||||
info.insert(2, f'coro=<{coro}>')
|
||||
|
||||
return info
|
||||
|
||||
|
||||
@reprlib.recursive_repr()
|
||||
def _task_repr(task):
|
||||
info = ' '.join(_task_repr_info(task))
|
||||
return f'<{task.__class__.__name__} {info}>'
|
||||
|
||||
|
||||
def _task_get_stack(task, limit):
|
||||
frames = []
|
||||
try:
|
||||
# 'async def' coroutines
|
||||
if hasattr(task._coro, 'cr_frame'):
|
||||
# case 1: 'async def' coroutines
|
||||
f = task._coro.cr_frame
|
||||
except AttributeError:
|
||||
elif hasattr(task._coro, 'gi_frame'):
|
||||
# case 2: legacy coroutines
|
||||
f = task._coro.gi_frame
|
||||
elif hasattr(task._coro, 'ag_frame'):
|
||||
# case 3: async generators
|
||||
f = task._coro.ag_frame
|
||||
else:
|
||||
# case 4: unknown objects
|
||||
f = None
|
||||
if f is not None:
|
||||
while f is not None:
|
||||
if limit is not None:
|
||||
@@ -61,15 +79,15 @@ def _task_print_stack(task, limit, file):
|
||||
linecache.checkcache(filename)
|
||||
line = linecache.getline(filename, lineno, f.f_globals)
|
||||
extracted_list.append((filename, lineno, name, line))
|
||||
|
||||
exc = task._exception
|
||||
if not extracted_list:
|
||||
print('No stack for %r' % task, file=file)
|
||||
print(f'No stack for {task!r}', file=file)
|
||||
elif exc is not None:
|
||||
print('Traceback for %r (most recent call last):' % task,
|
||||
file=file)
|
||||
print(f'Traceback for {task!r} (most recent call last):', file=file)
|
||||
else:
|
||||
print('Stack for %r (most recent call last):' % task,
|
||||
file=file)
|
||||
print(f'Stack for {task!r} (most recent call last):', file=file)
|
||||
|
||||
traceback.print_list(extracted_list, file=file)
|
||||
if exc is not None:
|
||||
for line in traceback.format_exception_only(exc.__class__, exc):
|
||||
|
||||
18
Lib/asyncio/compat.py
vendored
18
Lib/asyncio/compat.py
vendored
@@ -1,18 +0,0 @@
|
||||
"""Compatibility helpers for the different Python versions."""
|
||||
|
||||
import sys
|
||||
|
||||
PY34 = sys.version_info >= (3, 4)
|
||||
PY35 = sys.version_info >= (3, 5)
|
||||
PY352 = sys.version_info >= (3, 5, 2)
|
||||
|
||||
|
||||
def flatten_list_bytes(list_of_data):
|
||||
"""Concatenate a sequence of bytes-like objects."""
|
||||
if not PY34:
|
||||
# On Python 3.3 and older, bytes.join() doesn't handle
|
||||
# memoryview.
|
||||
list_of_data = (
|
||||
bytes(data) if isinstance(data, memoryview) else data
|
||||
for data in list_of_data)
|
||||
return b''.join(list_of_data)
|
||||
36
Lib/asyncio/constants.py
vendored
36
Lib/asyncio/constants.py
vendored
@@ -1,7 +1,41 @@
|
||||
"""Constants."""
|
||||
# Contains code from https://github.com/MagicStack/uvloop/tree/v0.16.0
|
||||
# SPDX-License-Identifier: PSF-2.0 AND (MIT OR Apache-2.0)
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2015-2021 MagicStack Inc. http://magic.io
|
||||
|
||||
import enum
|
||||
|
||||
# After the connection is lost, log warnings after this many write()s.
|
||||
LOG_THRESHOLD_FOR_CONNLOST_WRITES = 5
|
||||
|
||||
# Seconds to wait before retrying accept().
|
||||
ACCEPT_RETRY_DELAY = 1
|
||||
|
||||
# Number of stack entries to capture in debug mode.
|
||||
# The larger the number, the slower the operation in debug mode
|
||||
# (see extract_stack() in format_helpers.py).
|
||||
DEBUG_STACK_DEPTH = 10
|
||||
|
||||
# Number of seconds to wait for SSL handshake to complete
|
||||
# The default timeout matches that of Nginx.
|
||||
SSL_HANDSHAKE_TIMEOUT = 60.0
|
||||
|
||||
# Number of seconds to wait for SSL shutdown to complete
|
||||
# The default timeout mimics lingering_time
|
||||
SSL_SHUTDOWN_TIMEOUT = 30.0
|
||||
|
||||
# Used in sendfile fallback code. We use fallback for platforms
|
||||
# that don't support sendfile, or for TLS connections.
|
||||
SENDFILE_FALLBACK_READBUFFER_SIZE = 1024 * 256
|
||||
|
||||
FLOW_CONTROL_HIGH_WATER_SSL_READ = 256 # KiB
|
||||
FLOW_CONTROL_HIGH_WATER_SSL_WRITE = 512 # KiB
|
||||
|
||||
# Default timeout for joining the threads in the threadpool
|
||||
THREAD_JOIN_TIMEOUT = 300
|
||||
|
||||
# The enum should be here to break circular dependencies between
|
||||
# base_events and sslproto
|
||||
class _SendfileMode(enum.Enum):
|
||||
UNSUPPORTED = enum.auto()
|
||||
TRY_NATIVE = enum.auto()
|
||||
FALLBACK = enum.auto()
|
||||
|
||||
365
Lib/asyncio/coroutines.py
vendored
365
Lib/asyncio/coroutines.py
vendored
@@ -1,249 +1,16 @@
|
||||
__all__ = ['coroutine',
|
||||
'iscoroutinefunction', 'iscoroutine']
|
||||
__all__ = 'iscoroutinefunction', 'iscoroutine'
|
||||
|
||||
import functools
|
||||
import collections.abc
|
||||
import inspect
|
||||
import opcode
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
|
||||
from . import compat
|
||||
from . import events
|
||||
from . import base_futures
|
||||
from .log import logger
|
||||
|
||||
|
||||
# Opcode of "yield from" instruction
|
||||
_YIELD_FROM = opcode.opmap['YIELD_FROM']
|
||||
|
||||
# If you set _DEBUG to true, @coroutine will wrap the resulting
|
||||
# generator objects in a CoroWrapper instance (defined below). That
|
||||
# instance will log a message when the generator is never iterated
|
||||
# over, which may happen when you forget to use "yield from" with a
|
||||
# coroutine call. Note that the value of the _DEBUG flag is taken
|
||||
# when the decorator is used, so to be of any use it must be set
|
||||
# before you define your coroutines. A downside of using this feature
|
||||
# is that tracebacks show entries for the CoroWrapper.__next__ method
|
||||
# when _DEBUG is true.
|
||||
_DEBUG = (not sys.flags.ignore_environment and
|
||||
bool(os.environ.get('PYTHONASYNCIODEBUG')))
|
||||
|
||||
|
||||
try:
|
||||
_types_coroutine = types.coroutine
|
||||
_types_CoroutineType = types.CoroutineType
|
||||
except AttributeError:
|
||||
# Python 3.4
|
||||
_types_coroutine = None
|
||||
_types_CoroutineType = None
|
||||
|
||||
try:
|
||||
_inspect_iscoroutinefunction = inspect.iscoroutinefunction
|
||||
except AttributeError:
|
||||
# Python 3.4
|
||||
_inspect_iscoroutinefunction = lambda func: False
|
||||
|
||||
try:
|
||||
from collections.abc import Coroutine as _CoroutineABC, \
|
||||
Awaitable as _AwaitableABC
|
||||
except ImportError:
|
||||
_CoroutineABC = _AwaitableABC = None
|
||||
|
||||
|
||||
# Check for CPython issue #21209
|
||||
def has_yield_from_bug():
|
||||
class MyGen:
|
||||
def __init__(self):
|
||||
self.send_args = None
|
||||
def __iter__(self):
|
||||
return self
|
||||
def __next__(self):
|
||||
return 42
|
||||
def send(self, *what):
|
||||
self.send_args = what
|
||||
return None
|
||||
def yield_from_gen(gen):
|
||||
yield from gen
|
||||
value = (1, 2, 3)
|
||||
gen = MyGen()
|
||||
coro = yield_from_gen(gen)
|
||||
next(coro)
|
||||
coro.send(value)
|
||||
return gen.send_args != (value,)
|
||||
_YIELD_FROM_BUG = has_yield_from_bug()
|
||||
del has_yield_from_bug
|
||||
|
||||
|
||||
def debug_wrapper(gen):
|
||||
# This function is called from 'sys.set_coroutine_wrapper'.
|
||||
# We only wrap here coroutines defined via 'async def' syntax.
|
||||
# Generator-based coroutines are wrapped in @coroutine
|
||||
# decorator.
|
||||
return CoroWrapper(gen, None)
|
||||
|
||||
|
||||
class CoroWrapper:
|
||||
# Wrapper for coroutine object in _DEBUG mode.
|
||||
|
||||
def __init__(self, gen, func=None):
|
||||
assert inspect.isgenerator(gen) or inspect.iscoroutine(gen), gen
|
||||
self.gen = gen
|
||||
self.func = func # Used to unwrap @coroutine decorator
|
||||
self._source_traceback = traceback.extract_stack(sys._getframe(1))
|
||||
self.__name__ = getattr(gen, '__name__', None)
|
||||
self.__qualname__ = getattr(gen, '__qualname__', None)
|
||||
|
||||
def __repr__(self):
|
||||
coro_repr = _format_coroutine(self)
|
||||
if self._source_traceback:
|
||||
frame = self._source_traceback[-1]
|
||||
coro_repr += ', created at %s:%s' % (frame[0], frame[1])
|
||||
return '<%s %s>' % (self.__class__.__name__, coro_repr)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return self.gen.send(None)
|
||||
|
||||
if _YIELD_FROM_BUG:
|
||||
# For for CPython issue #21209: using "yield from" and a custom
|
||||
# generator, generator.send(tuple) unpacks the tuple instead of passing
|
||||
# the tuple unchanged. Check if the caller is a generator using "yield
|
||||
# from" to decide if the parameter should be unpacked or not.
|
||||
def send(self, *value):
|
||||
frame = sys._getframe()
|
||||
caller = frame.f_back
|
||||
assert caller.f_lasti >= 0
|
||||
if caller.f_code.co_code[caller.f_lasti] != _YIELD_FROM:
|
||||
value = value[0]
|
||||
return self.gen.send(value)
|
||||
else:
|
||||
def send(self, value):
|
||||
return self.gen.send(value)
|
||||
|
||||
def throw(self, type, value=None, traceback=None):
|
||||
return self.gen.throw(type, value, traceback)
|
||||
|
||||
def close(self):
|
||||
return self.gen.close()
|
||||
|
||||
@property
|
||||
def gi_frame(self):
|
||||
return self.gen.gi_frame
|
||||
|
||||
@property
|
||||
def gi_running(self):
|
||||
return self.gen.gi_running
|
||||
|
||||
@property
|
||||
def gi_code(self):
|
||||
return self.gen.gi_code
|
||||
|
||||
if compat.PY35:
|
||||
|
||||
def __await__(self):
|
||||
cr_await = getattr(self.gen, 'cr_await', None)
|
||||
if cr_await is not None:
|
||||
raise RuntimeError(
|
||||
"Cannot await on coroutine {!r} while it's "
|
||||
"awaiting for {!r}".format(self.gen, cr_await))
|
||||
return self
|
||||
|
||||
@property
|
||||
def gi_yieldfrom(self):
|
||||
return self.gen.gi_yieldfrom
|
||||
|
||||
@property
|
||||
def cr_await(self):
|
||||
return self.gen.cr_await
|
||||
|
||||
@property
|
||||
def cr_running(self):
|
||||
return self.gen.cr_running
|
||||
|
||||
@property
|
||||
def cr_code(self):
|
||||
return self.gen.cr_code
|
||||
|
||||
@property
|
||||
def cr_frame(self):
|
||||
return self.gen.cr_frame
|
||||
|
||||
def __del__(self):
|
||||
# Be careful accessing self.gen.frame -- self.gen might not exist.
|
||||
gen = getattr(self, 'gen', None)
|
||||
frame = getattr(gen, 'gi_frame', None)
|
||||
if frame is None:
|
||||
frame = getattr(gen, 'cr_frame', None)
|
||||
if frame is not None and frame.f_lasti == -1:
|
||||
msg = '%r was never yielded from' % self
|
||||
tb = getattr(self, '_source_traceback', ())
|
||||
if tb:
|
||||
tb = ''.join(traceback.format_list(tb))
|
||||
msg += ('\nCoroutine object created at '
|
||||
'(most recent call last):\n')
|
||||
msg += tb.rstrip()
|
||||
logger.error(msg)
|
||||
|
||||
|
||||
def coroutine(func):
|
||||
"""Decorator to mark coroutines.
|
||||
|
||||
If the coroutine is not yielded from before it is destroyed,
|
||||
an error message is logged.
|
||||
"""
|
||||
if _inspect_iscoroutinefunction(func):
|
||||
# In Python 3.5 that's all we need to do for coroutines
|
||||
# defiend with "async def".
|
||||
# Wrapping in CoroWrapper will happen via
|
||||
# 'sys.set_coroutine_wrapper' function.
|
||||
return func
|
||||
|
||||
if inspect.isgeneratorfunction(func):
|
||||
coro = func
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def coro(*args, **kw):
|
||||
res = func(*args, **kw)
|
||||
if (base_futures.isfuture(res) or inspect.isgenerator(res) or
|
||||
isinstance(res, CoroWrapper)):
|
||||
res = yield from res
|
||||
elif _AwaitableABC is not None:
|
||||
# If 'func' returns an Awaitable (new in 3.5) we
|
||||
# want to run it.
|
||||
try:
|
||||
await_meth = res.__await__
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(res, _AwaitableABC):
|
||||
res = yield from await_meth()
|
||||
return res
|
||||
|
||||
if not _DEBUG:
|
||||
if _types_coroutine is None:
|
||||
wrapper = coro
|
||||
else:
|
||||
wrapper = _types_coroutine(coro)
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwds):
|
||||
w = CoroWrapper(coro(*args, **kwds), func=func)
|
||||
if w._source_traceback:
|
||||
del w._source_traceback[-1]
|
||||
# Python < 3.5 does not implement __qualname__
|
||||
# on generator objects, so we set it manually.
|
||||
# We use getattr as some callables (such as
|
||||
# functools.partial may lack __qualname__).
|
||||
w.__name__ = getattr(func, '__name__', None)
|
||||
w.__qualname__ = getattr(func, '__qualname__', None)
|
||||
return w
|
||||
|
||||
wrapper._is_coroutine = _is_coroutine # For iscoroutinefunction().
|
||||
return wrapper
|
||||
def _is_debug_mode():
|
||||
# See: https://docs.python.org/3/library/asyncio-dev.html#asyncio-debug-mode.
|
||||
return sys.flags.dev_mode or (not sys.flags.ignore_environment and
|
||||
bool(os.environ.get('PYTHONASYNCIODEBUG')))
|
||||
|
||||
|
||||
# A marker for iscoroutinefunction.
|
||||
@@ -252,93 +19,91 @@ _is_coroutine = object()
|
||||
|
||||
def iscoroutinefunction(func):
|
||||
"""Return True if func is a decorated coroutine function."""
|
||||
return (getattr(func, '_is_coroutine', None) is _is_coroutine or
|
||||
_inspect_iscoroutinefunction(func))
|
||||
return (inspect.iscoroutinefunction(func) or
|
||||
getattr(func, '_is_coroutine', None) is _is_coroutine)
|
||||
|
||||
|
||||
_COROUTINE_TYPES = (types.GeneratorType, CoroWrapper)
|
||||
if _CoroutineABC is not None:
|
||||
_COROUTINE_TYPES += (_CoroutineABC,)
|
||||
if _types_CoroutineType is not None:
|
||||
# Prioritize native coroutine check to speed-up
|
||||
# asyncio.iscoroutine.
|
||||
_COROUTINE_TYPES = (_types_CoroutineType,) + _COROUTINE_TYPES
|
||||
# Prioritize native coroutine check to speed-up
|
||||
# asyncio.iscoroutine.
|
||||
_COROUTINE_TYPES = (types.CoroutineType, collections.abc.Coroutine)
|
||||
_iscoroutine_typecache = set()
|
||||
|
||||
|
||||
def iscoroutine(obj):
|
||||
"""Return True if obj is a coroutine object."""
|
||||
return isinstance(obj, _COROUTINE_TYPES)
|
||||
if type(obj) in _iscoroutine_typecache:
|
||||
return True
|
||||
|
||||
if isinstance(obj, _COROUTINE_TYPES):
|
||||
# Just in case we don't want to cache more than 100
|
||||
# positive types. That shouldn't ever happen, unless
|
||||
# someone stressing the system on purpose.
|
||||
if len(_iscoroutine_typecache) < 100:
|
||||
_iscoroutine_typecache.add(type(obj))
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _format_coroutine(coro):
|
||||
assert iscoroutine(coro)
|
||||
|
||||
if not hasattr(coro, 'cr_code') and not hasattr(coro, 'gi_code'):
|
||||
# Most likely a built-in type or a Cython coroutine.
|
||||
def get_name(coro):
|
||||
# Coroutines compiled with Cython sometimes don't have
|
||||
# proper __qualname__ or __name__. While that is a bug
|
||||
# in Cython, asyncio shouldn't crash with an AttributeError
|
||||
# in its __repr__ functions.
|
||||
if hasattr(coro, '__qualname__') and coro.__qualname__:
|
||||
coro_name = coro.__qualname__
|
||||
elif hasattr(coro, '__name__') and coro.__name__:
|
||||
coro_name = coro.__name__
|
||||
else:
|
||||
# Stop masking Cython bugs, expose them in a friendly way.
|
||||
coro_name = f'<{type(coro).__name__} without __name__>'
|
||||
return f'{coro_name}()'
|
||||
|
||||
# Built-in types might not have __qualname__ or __name__.
|
||||
coro_name = getattr(
|
||||
coro, '__qualname__',
|
||||
getattr(coro, '__name__', type(coro).__name__))
|
||||
coro_name = '{}()'.format(coro_name)
|
||||
|
||||
running = False
|
||||
def is_running(coro):
|
||||
try:
|
||||
running = coro.cr_running
|
||||
return coro.cr_running
|
||||
except AttributeError:
|
||||
try:
|
||||
running = coro.gi_running
|
||||
return coro.gi_running
|
||||
except AttributeError:
|
||||
pass
|
||||
return False
|
||||
|
||||
if running:
|
||||
return '{} running'.format(coro_name)
|
||||
coro_code = None
|
||||
if hasattr(coro, 'cr_code') and coro.cr_code:
|
||||
coro_code = coro.cr_code
|
||||
elif hasattr(coro, 'gi_code') and coro.gi_code:
|
||||
coro_code = coro.gi_code
|
||||
|
||||
coro_name = get_name(coro)
|
||||
|
||||
if not coro_code:
|
||||
# Built-in types might not have __qualname__ or __name__.
|
||||
if is_running(coro):
|
||||
return f'{coro_name} running'
|
||||
else:
|
||||
return coro_name
|
||||
|
||||
coro_name = None
|
||||
if isinstance(coro, CoroWrapper):
|
||||
func = coro.func
|
||||
coro_name = coro.__qualname__
|
||||
if coro_name is not None:
|
||||
coro_name = '{}()'.format(coro_name)
|
||||
else:
|
||||
func = coro
|
||||
|
||||
if coro_name is None:
|
||||
coro_name = events._format_callback(func, (), {})
|
||||
|
||||
try:
|
||||
coro_code = coro.gi_code
|
||||
except AttributeError:
|
||||
coro_code = coro.cr_code
|
||||
|
||||
try:
|
||||
coro_frame = None
|
||||
if hasattr(coro, 'gi_frame') and coro.gi_frame:
|
||||
coro_frame = coro.gi_frame
|
||||
except AttributeError:
|
||||
elif hasattr(coro, 'cr_frame') and coro.cr_frame:
|
||||
coro_frame = coro.cr_frame
|
||||
|
||||
filename = coro_code.co_filename
|
||||
# If Cython's coroutine has a fake code object without proper
|
||||
# co_filename -- expose that.
|
||||
filename = coro_code.co_filename or '<empty co_filename>'
|
||||
|
||||
lineno = 0
|
||||
if (isinstance(coro, CoroWrapper) and
|
||||
not inspect.isgeneratorfunction(coro.func) and
|
||||
coro.func is not None):
|
||||
source = events._get_function_source(coro.func)
|
||||
if source is not None:
|
||||
filename, lineno = source
|
||||
if coro_frame is None:
|
||||
coro_repr = ('%s done, defined at %s:%s'
|
||||
% (coro_name, filename, lineno))
|
||||
else:
|
||||
coro_repr = ('%s running, defined at %s:%s'
|
||||
% (coro_name, filename, lineno))
|
||||
elif coro_frame is not None:
|
||||
|
||||
if coro_frame is not None:
|
||||
lineno = coro_frame.f_lineno
|
||||
coro_repr = ('%s running at %s:%s'
|
||||
% (coro_name, filename, lineno))
|
||||
coro_repr = f'{coro_name} running at {filename}:{lineno}'
|
||||
|
||||
else:
|
||||
lineno = coro_code.co_firstlineno
|
||||
coro_repr = ('%s done, defined at %s:%s'
|
||||
% (coro_name, filename, lineno))
|
||||
coro_repr = f'{coro_name} done, defined at {filename}:{lineno}'
|
||||
|
||||
return coro_repr
|
||||
|
||||
469
Lib/asyncio/events.py
vendored
469
Lib/asyncio/events.py
vendored
@@ -1,96 +1,50 @@
|
||||
"""Event loop and event loop policy."""
|
||||
|
||||
__all__ = ['AbstractEventLoopPolicy',
|
||||
'AbstractEventLoop', 'AbstractServer',
|
||||
'Handle', 'TimerHandle',
|
||||
'get_event_loop_policy', 'set_event_loop_policy',
|
||||
'get_event_loop', 'set_event_loop', 'new_event_loop',
|
||||
'get_child_watcher', 'set_child_watcher',
|
||||
'_set_running_loop', 'get_running_loop',
|
||||
'_get_running_loop',
|
||||
]
|
||||
# Contains code from https://github.com/MagicStack/uvloop/tree/v0.16.0
|
||||
# SPDX-License-Identifier: PSF-2.0 AND (MIT OR Apache-2.0)
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2015-2021 MagicStack Inc. http://magic.io
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import reprlib
|
||||
__all__ = (
|
||||
'AbstractEventLoopPolicy',
|
||||
'AbstractEventLoop', 'AbstractServer',
|
||||
'Handle', 'TimerHandle',
|
||||
'get_event_loop_policy', 'set_event_loop_policy',
|
||||
'get_event_loop', 'set_event_loop', 'new_event_loop',
|
||||
'get_child_watcher', 'set_child_watcher',
|
||||
'_set_running_loop', 'get_running_loop',
|
||||
'_get_running_loop',
|
||||
)
|
||||
|
||||
import contextvars
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
from asyncio import compat
|
||||
|
||||
|
||||
def _get_function_source(func):
|
||||
if compat.PY34:
|
||||
func = inspect.unwrap(func)
|
||||
elif hasattr(func, '__wrapped__'):
|
||||
func = func.__wrapped__
|
||||
if inspect.isfunction(func):
|
||||
code = func.__code__
|
||||
return (code.co_filename, code.co_firstlineno)
|
||||
if isinstance(func, functools.partial):
|
||||
return _get_function_source(func.func)
|
||||
if compat.PY34 and isinstance(func, functools.partialmethod):
|
||||
return _get_function_source(func.func)
|
||||
return None
|
||||
|
||||
|
||||
def _format_args_and_kwargs(args, kwargs):
|
||||
"""Format function arguments and keyword arguments.
|
||||
|
||||
Special case for a single parameter: ('hello',) is formatted as ('hello').
|
||||
"""
|
||||
# use reprlib to limit the length of the output
|
||||
items = []
|
||||
if args:
|
||||
items.extend(reprlib.repr(arg) for arg in args)
|
||||
if kwargs:
|
||||
items.extend('{}={}'.format(k, reprlib.repr(v))
|
||||
for k, v in kwargs.items())
|
||||
return '(' + ', '.join(items) + ')'
|
||||
|
||||
|
||||
def _format_callback(func, args, kwargs, suffix=''):
|
||||
if isinstance(func, functools.partial):
|
||||
suffix = _format_args_and_kwargs(args, kwargs) + suffix
|
||||
return _format_callback(func.func, func.args, func.keywords, suffix)
|
||||
|
||||
if hasattr(func, '__qualname__'):
|
||||
func_repr = getattr(func, '__qualname__')
|
||||
elif hasattr(func, '__name__'):
|
||||
func_repr = getattr(func, '__name__')
|
||||
else:
|
||||
func_repr = repr(func)
|
||||
|
||||
func_repr += _format_args_and_kwargs(args, kwargs)
|
||||
if suffix:
|
||||
func_repr += suffix
|
||||
return func_repr
|
||||
|
||||
def _format_callback_source(func, args):
|
||||
func_repr = _format_callback(func, args, None)
|
||||
source = _get_function_source(func)
|
||||
if source:
|
||||
func_repr += ' at %s:%s' % source
|
||||
return func_repr
|
||||
from . import format_helpers
|
||||
|
||||
|
||||
class Handle:
|
||||
"""Object returned by callback registration methods."""
|
||||
|
||||
__slots__ = ('_callback', '_args', '_cancelled', '_loop',
|
||||
'_source_traceback', '_repr', '__weakref__')
|
||||
'_source_traceback', '_repr', '__weakref__',
|
||||
'_context')
|
||||
|
||||
def __init__(self, callback, args, loop):
|
||||
def __init__(self, callback, args, loop, context=None):
|
||||
if context is None:
|
||||
context = contextvars.copy_context()
|
||||
self._context = context
|
||||
self._loop = loop
|
||||
self._callback = callback
|
||||
self._args = args
|
||||
self._cancelled = False
|
||||
self._repr = None
|
||||
if self._loop.get_debug():
|
||||
self._source_traceback = traceback.extract_stack(sys._getframe(1))
|
||||
self._source_traceback = format_helpers.extract_stack(
|
||||
sys._getframe(1))
|
||||
else:
|
||||
self._source_traceback = None
|
||||
|
||||
@@ -99,17 +53,21 @@ class Handle:
|
||||
if self._cancelled:
|
||||
info.append('cancelled')
|
||||
if self._callback is not None:
|
||||
info.append(_format_callback_source(self._callback, self._args))
|
||||
info.append(format_helpers._format_callback_source(
|
||||
self._callback, self._args))
|
||||
if self._source_traceback:
|
||||
frame = self._source_traceback[-1]
|
||||
info.append('created at %s:%s' % (frame[0], frame[1]))
|
||||
info.append(f'created at {frame[0]}:{frame[1]}')
|
||||
return info
|
||||
|
||||
def __repr__(self):
|
||||
if self._repr is not None:
|
||||
return self._repr
|
||||
info = self._repr_info()
|
||||
return '<%s>' % ' '.join(info)
|
||||
return '<{}>'.format(' '.join(info))
|
||||
|
||||
def get_context(self):
|
||||
return self._context
|
||||
|
||||
def cancel(self):
|
||||
if not self._cancelled:
|
||||
@@ -122,12 +80,18 @@ class Handle:
|
||||
self._callback = None
|
||||
self._args = None
|
||||
|
||||
def cancelled(self):
|
||||
return self._cancelled
|
||||
|
||||
def _run(self):
|
||||
try:
|
||||
self._callback(*self._args)
|
||||
except Exception as exc:
|
||||
cb = _format_callback_source(self._callback, self._args)
|
||||
msg = 'Exception in callback {}'.format(cb)
|
||||
self._context.run(self._callback, *self._args)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
cb = format_helpers._format_callback_source(
|
||||
self._callback, self._args)
|
||||
msg = f'Exception in callback {cb}'
|
||||
context = {
|
||||
'message': msg,
|
||||
'exception': exc,
|
||||
@@ -144,9 +108,8 @@ class TimerHandle(Handle):
|
||||
|
||||
__slots__ = ['_scheduled', '_when']
|
||||
|
||||
def __init__(self, when, callback, args, loop):
|
||||
assert when is not None
|
||||
super().__init__(callback, args, loop)
|
||||
def __init__(self, when, callback, args, loop, context=None):
|
||||
super().__init__(callback, args, loop, context)
|
||||
if self._source_traceback:
|
||||
del self._source_traceback[-1]
|
||||
self._when = when
|
||||
@@ -155,27 +118,31 @@ class TimerHandle(Handle):
|
||||
def _repr_info(self):
|
||||
info = super()._repr_info()
|
||||
pos = 2 if self._cancelled else 1
|
||||
info.insert(pos, 'when=%s' % self._when)
|
||||
info.insert(pos, f'when={self._when}')
|
||||
return info
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._when)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._when < other._when
|
||||
if isinstance(other, TimerHandle):
|
||||
return self._when < other._when
|
||||
return NotImplemented
|
||||
|
||||
def __le__(self, other):
|
||||
if self._when < other._when:
|
||||
return True
|
||||
return self.__eq__(other)
|
||||
if isinstance(other, TimerHandle):
|
||||
return self._when < other._when or self.__eq__(other)
|
||||
return NotImplemented
|
||||
|
||||
def __gt__(self, other):
|
||||
return self._when > other._when
|
||||
if isinstance(other, TimerHandle):
|
||||
return self._when > other._when
|
||||
return NotImplemented
|
||||
|
||||
def __ge__(self, other):
|
||||
if self._when > other._when:
|
||||
return True
|
||||
return self.__eq__(other)
|
||||
if isinstance(other, TimerHandle):
|
||||
return self._when > other._when or self.__eq__(other)
|
||||
return NotImplemented
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, TimerHandle):
|
||||
@@ -185,26 +152,60 @@ class TimerHandle(Handle):
|
||||
self._cancelled == other._cancelled)
|
||||
return NotImplemented
|
||||
|
||||
def __ne__(self, other):
|
||||
equal = self.__eq__(other)
|
||||
return NotImplemented if equal is NotImplemented else not equal
|
||||
|
||||
def cancel(self):
|
||||
if not self._cancelled:
|
||||
self._loop._timer_handle_cancelled(self)
|
||||
super().cancel()
|
||||
|
||||
def when(self):
|
||||
"""Return a scheduled callback time.
|
||||
|
||||
The time is an absolute timestamp, using the same time
|
||||
reference as loop.time().
|
||||
"""
|
||||
return self._when
|
||||
|
||||
|
||||
class AbstractServer:
|
||||
"""Abstract server returned by create_server()."""
|
||||
|
||||
def close(self):
|
||||
"""Stop serving. This leaves existing connections open."""
|
||||
return NotImplemented
|
||||
raise NotImplementedError
|
||||
|
||||
def wait_closed(self):
|
||||
def get_loop(self):
|
||||
"""Get the event loop the Server object is attached to."""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_serving(self):
|
||||
"""Return True if the server is accepting connections."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def start_serving(self):
|
||||
"""Start accepting connections.
|
||||
|
||||
This method is idempotent, so it can be called when
|
||||
the server is already being serving.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def serve_forever(self):
|
||||
"""Start accepting connections until the coroutine is cancelled.
|
||||
|
||||
The server is closed when the coroutine is cancelled.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def wait_closed(self):
|
||||
"""Coroutine to wait until service is closed."""
|
||||
return NotImplemented
|
||||
raise NotImplementedError
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
self.close()
|
||||
await self.wait_closed()
|
||||
|
||||
|
||||
class AbstractEventLoop:
|
||||
@@ -250,23 +251,27 @@ class AbstractEventLoop:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def shutdown_asyncgens(self):
|
||||
async def shutdown_asyncgens(self):
|
||||
"""Shutdown all active asynchronous generators."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def shutdown_default_executor(self):
|
||||
"""Schedule the shutdown of the default executor."""
|
||||
raise NotImplementedError
|
||||
|
||||
# Methods scheduling callbacks. All these return Handles.
|
||||
|
||||
def _timer_handle_cancelled(self, handle):
|
||||
"""Notification that a TimerHandle has been cancelled."""
|
||||
raise NotImplementedError
|
||||
|
||||
def call_soon(self, callback, *args):
|
||||
return self.call_later(0, callback, *args)
|
||||
def call_soon(self, callback, *args, context=None):
|
||||
return self.call_later(0, callback, *args, context=context)
|
||||
|
||||
def call_later(self, delay, callback, *args):
|
||||
def call_later(self, delay, callback, *args, context=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def call_at(self, when, callback, *args):
|
||||
def call_at(self, when, callback, *args, context=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def time(self):
|
||||
@@ -277,12 +282,12 @@ class AbstractEventLoop:
|
||||
|
||||
# Method scheduling a coroutine object: create a task.
|
||||
|
||||
def create_task(self, coro):
|
||||
def create_task(self, coro, *, name=None, context=None):
|
||||
raise NotImplementedError
|
||||
|
||||
# Methods for interacting with threads.
|
||||
|
||||
def call_soon_threadsafe(self, callback, *args):
|
||||
def call_soon_threadsafe(self, callback, *args, context=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def run_in_executor(self, executor, func, *args):
|
||||
@@ -293,21 +298,31 @@ class AbstractEventLoop:
|
||||
|
||||
# Network I/O methods returning Futures.
|
||||
|
||||
def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0):
|
||||
async def getaddrinfo(self, host, port, *,
|
||||
family=0, type=0, proto=0, flags=0):
|
||||
raise NotImplementedError
|
||||
|
||||
def getnameinfo(self, sockaddr, flags=0):
|
||||
async def getnameinfo(self, sockaddr, flags=0):
|
||||
raise NotImplementedError
|
||||
|
||||
def create_connection(self, protocol_factory, host=None, port=None, *,
|
||||
ssl=None, family=0, proto=0, flags=0, sock=None,
|
||||
local_addr=None, server_hostname=None):
|
||||
async def create_connection(
|
||||
self, protocol_factory, host=None, port=None,
|
||||
*, ssl=None, family=0, proto=0,
|
||||
flags=0, sock=None, local_addr=None,
|
||||
server_hostname=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None,
|
||||
happy_eyeballs_delay=None, interleave=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def create_server(self, protocol_factory, host=None, port=None, *,
|
||||
family=socket.AF_UNSPEC, flags=socket.AI_PASSIVE,
|
||||
sock=None, backlog=100, ssl=None, reuse_address=None,
|
||||
reuse_port=None):
|
||||
async def create_server(
|
||||
self, protocol_factory, host=None, port=None,
|
||||
*, family=socket.AF_UNSPEC,
|
||||
flags=socket.AI_PASSIVE, sock=None, backlog=100,
|
||||
ssl=None, reuse_address=None, reuse_port=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None,
|
||||
start_serving=True):
|
||||
"""A coroutine which creates a TCP server bound to host and port.
|
||||
|
||||
The return value is a Server object which can be used to stop
|
||||
@@ -315,8 +330,8 @@ class AbstractEventLoop:
|
||||
|
||||
If host is an empty string or None all interfaces are assumed
|
||||
and a list of multiple sockets will be returned (most likely
|
||||
one for IPv4 and another one for IPv6). The host parameter can also be a
|
||||
sequence (e.g. list) of hosts to bind to.
|
||||
one for IPv4 and another one for IPv6). The host parameter can also be
|
||||
a sequence (e.g. list) of hosts to bind to.
|
||||
|
||||
family can be set to either AF_INET or AF_INET6 to force the
|
||||
socket to use IPv4 or IPv6. If not set it will be determined
|
||||
@@ -342,22 +357,62 @@ class AbstractEventLoop:
|
||||
the same port as other existing endpoints are bound to, so long as
|
||||
they all set this flag when being created. This option is not
|
||||
supported on Windows.
|
||||
|
||||
ssl_handshake_timeout is the time in seconds that an SSL server
|
||||
will wait for completion of the SSL handshake before aborting the
|
||||
connection. Default is 60s.
|
||||
|
||||
ssl_shutdown_timeout is the time in seconds that an SSL server
|
||||
will wait for completion of the SSL shutdown procedure
|
||||
before aborting the connection. Default is 30s.
|
||||
|
||||
start_serving set to True (default) causes the created server
|
||||
to start accepting connections immediately. When set to False,
|
||||
the user should await Server.start_serving() or Server.serve_forever()
|
||||
to make the server to start accepting connections.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_unix_connection(self, protocol_factory, path, *,
|
||||
ssl=None, sock=None,
|
||||
server_hostname=None):
|
||||
async def sendfile(self, transport, file, offset=0, count=None,
|
||||
*, fallback=True):
|
||||
"""Send a file through a transport.
|
||||
|
||||
Return an amount of sent bytes.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_unix_server(self, protocol_factory, path, *,
|
||||
sock=None, backlog=100, ssl=None):
|
||||
async def start_tls(self, transport, protocol, sslcontext, *,
|
||||
server_side=False,
|
||||
server_hostname=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None):
|
||||
"""Upgrade a transport to TLS.
|
||||
|
||||
Return a new transport that *protocol* should start using
|
||||
immediately.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def create_unix_connection(
|
||||
self, protocol_factory, path=None, *,
|
||||
ssl=None, sock=None,
|
||||
server_hostname=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None):
|
||||
raise NotImplementedError
|
||||
|
||||
async def create_unix_server(
|
||||
self, protocol_factory, path=None, *,
|
||||
sock=None, backlog=100, ssl=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None,
|
||||
start_serving=True):
|
||||
"""A coroutine which creates a UNIX Domain Socket server.
|
||||
|
||||
The return value is a Server object, which can be used to stop
|
||||
the service.
|
||||
|
||||
path is a str, representing a file systsem path to bind the
|
||||
path is a str, representing a file system path to bind the
|
||||
server socket to.
|
||||
|
||||
sock can optionally be specified in order to use a preexisting
|
||||
@@ -368,14 +423,40 @@ class AbstractEventLoop:
|
||||
|
||||
ssl can be set to an SSLContext to enable SSL over the
|
||||
accepted connections.
|
||||
|
||||
ssl_handshake_timeout is the time in seconds that an SSL server
|
||||
will wait for the SSL handshake to complete (defaults to 60s).
|
||||
|
||||
ssl_shutdown_timeout is the time in seconds that an SSL server
|
||||
will wait for the SSL shutdown to finish (defaults to 30s).
|
||||
|
||||
start_serving set to True (default) causes the created server
|
||||
to start accepting connections immediately. When set to False,
|
||||
the user should await Server.start_serving() or Server.serve_forever()
|
||||
to make the server to start accepting connections.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create_datagram_endpoint(self, protocol_factory,
|
||||
local_addr=None, remote_addr=None, *,
|
||||
family=0, proto=0, flags=0,
|
||||
reuse_address=None, reuse_port=None,
|
||||
allow_broadcast=None, sock=None):
|
||||
async def connect_accepted_socket(
|
||||
self, protocol_factory, sock,
|
||||
*, ssl=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None):
|
||||
"""Handle an accepted connection.
|
||||
|
||||
This is used by servers that accept connections outside of
|
||||
asyncio, but use asyncio to handle connections.
|
||||
|
||||
This method is a coroutine. When completed, the coroutine
|
||||
returns a (transport, protocol) pair.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def create_datagram_endpoint(self, protocol_factory,
|
||||
local_addr=None, remote_addr=None, *,
|
||||
family=0, proto=0, flags=0,
|
||||
reuse_address=None, reuse_port=None,
|
||||
allow_broadcast=None, sock=None):
|
||||
"""A coroutine which creates a datagram endpoint.
|
||||
|
||||
This method will try to establish the endpoint in the background.
|
||||
@@ -383,8 +464,8 @@ class AbstractEventLoop:
|
||||
|
||||
protocol_factory must be a callable returning a protocol instance.
|
||||
|
||||
socket family AF_INET or socket.AF_INET6 depending on host (or
|
||||
family if specified), socket type SOCK_DGRAM.
|
||||
socket family AF_INET, socket.AF_INET6 or socket.AF_UNIX depending on
|
||||
host (or family if specified), socket type SOCK_DGRAM.
|
||||
|
||||
reuse_address tells the kernel to reuse a local socket in
|
||||
TIME_WAIT state, without waiting for its natural timeout to
|
||||
@@ -408,7 +489,7 @@ class AbstractEventLoop:
|
||||
|
||||
# Pipes and subprocesses.
|
||||
|
||||
def connect_read_pipe(self, protocol_factory, pipe):
|
||||
async def connect_read_pipe(self, protocol_factory, pipe):
|
||||
"""Register read pipe in event loop. Set the pipe to non-blocking mode.
|
||||
|
||||
protocol_factory should instantiate object with Protocol interface.
|
||||
@@ -418,10 +499,10 @@ class AbstractEventLoop:
|
||||
# The reason to accept file-like object instead of just file descriptor
|
||||
# is: we need to own pipe and close it at transport finishing
|
||||
# Can got complicated errors if pass f.fileno(),
|
||||
# close fd in pipe transport then close f and vise versa.
|
||||
# close fd in pipe transport then close f and vice versa.
|
||||
raise NotImplementedError
|
||||
|
||||
def connect_write_pipe(self, protocol_factory, pipe):
|
||||
async def connect_write_pipe(self, protocol_factory, pipe):
|
||||
"""Register write pipe in event loop.
|
||||
|
||||
protocol_factory should instantiate object with BaseProtocol interface.
|
||||
@@ -431,17 +512,21 @@ class AbstractEventLoop:
|
||||
# The reason to accept file-like object instead of just file descriptor
|
||||
# is: we need to own pipe and close it at transport finishing
|
||||
# Can got complicated errors if pass f.fileno(),
|
||||
# close fd in pipe transport then close f and vise versa.
|
||||
# close fd in pipe transport then close f and vice versa.
|
||||
raise NotImplementedError
|
||||
|
||||
def subprocess_shell(self, protocol_factory, cmd, *, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
**kwargs):
|
||||
async def subprocess_shell(self, protocol_factory, cmd, *,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
**kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def subprocess_exec(self, protocol_factory, *args, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
**kwargs):
|
||||
async def subprocess_exec(self, protocol_factory, *args,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
**kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
# Ready-based callback registration methods.
|
||||
@@ -463,16 +548,32 @@ class AbstractEventLoop:
|
||||
|
||||
# Completion based I/O methods returning Futures.
|
||||
|
||||
def sock_recv(self, sock, nbytes):
|
||||
async def sock_recv(self, sock, nbytes):
|
||||
raise NotImplementedError
|
||||
|
||||
def sock_sendall(self, sock, data):
|
||||
async def sock_recv_into(self, sock, buf):
|
||||
raise NotImplementedError
|
||||
|
||||
def sock_connect(self, sock, address):
|
||||
async def sock_recvfrom(self, sock, bufsize):
|
||||
raise NotImplementedError
|
||||
|
||||
def sock_accept(self, sock):
|
||||
async def sock_recvfrom_into(self, sock, buf, nbytes=0):
|
||||
raise NotImplementedError
|
||||
|
||||
async def sock_sendall(self, sock, data):
|
||||
raise NotImplementedError
|
||||
|
||||
async def sock_sendto(self, sock, data, address):
|
||||
raise NotImplementedError
|
||||
|
||||
async def sock_connect(self, sock, address):
|
||||
raise NotImplementedError
|
||||
|
||||
async def sock_accept(self, sock):
|
||||
raise NotImplementedError
|
||||
|
||||
async def sock_sendfile(self, sock, file, offset=0, count=None,
|
||||
*, fallback=None):
|
||||
raise NotImplementedError
|
||||
|
||||
# Signal handling.
|
||||
@@ -520,7 +621,7 @@ class AbstractEventLoopPolicy:
|
||||
def get_event_loop(self):
|
||||
"""Get the event loop for the current context.
|
||||
|
||||
Returns an event loop object implementing the BaseEventLoop interface,
|
||||
Returns an event loop object implementing the AbstractEventLoop interface,
|
||||
or raises an exception in case no event loop has been set for the
|
||||
current context and the current policy does not specify to create one.
|
||||
|
||||
@@ -571,23 +672,43 @@ class BaseDefaultEventLoopPolicy(AbstractEventLoopPolicy):
|
||||
self._local = self._Local()
|
||||
|
||||
def get_event_loop(self):
|
||||
"""Get the event loop.
|
||||
"""Get the event loop for the current context.
|
||||
|
||||
This may be None or an instance of EventLoop.
|
||||
Returns an instance of EventLoop or raises an exception.
|
||||
"""
|
||||
if (self._local._loop is None and
|
||||
not self._local._set_called and
|
||||
isinstance(threading.current_thread(), threading._MainThread)):
|
||||
not self._local._set_called and
|
||||
threading.current_thread() is threading.main_thread()):
|
||||
stacklevel = 2
|
||||
try:
|
||||
f = sys._getframe(1)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
# Move up the call stack so that the warning is attached
|
||||
# to the line outside asyncio itself.
|
||||
while f:
|
||||
module = f.f_globals.get('__name__')
|
||||
if not (module == 'asyncio' or module.startswith('asyncio.')):
|
||||
break
|
||||
f = f.f_back
|
||||
stacklevel += 1
|
||||
import warnings
|
||||
warnings.warn('There is no current event loop',
|
||||
DeprecationWarning, stacklevel=stacklevel)
|
||||
self.set_event_loop(self.new_event_loop())
|
||||
|
||||
if self._local._loop is None:
|
||||
raise RuntimeError('There is no current event loop in thread %r.'
|
||||
% threading.current_thread().name)
|
||||
|
||||
return self._local._loop
|
||||
|
||||
def set_event_loop(self, loop):
|
||||
"""Set the event loop."""
|
||||
self._local._set_called = True
|
||||
assert loop is None or isinstance(loop, AbstractEventLoop)
|
||||
if loop is not None and not isinstance(loop, AbstractEventLoop):
|
||||
raise TypeError(f"loop must be an instance of AbstractEventLoop or None, not '{type(loop).__name__}'")
|
||||
self._local._loop = loop
|
||||
|
||||
def new_event_loop(self):
|
||||
@@ -611,7 +732,9 @@ _lock = threading.Lock()
|
||||
|
||||
# A TLS for the running event loop, used by _get_running_loop.
|
||||
class _RunningLoop(threading.local):
|
||||
_loop = None
|
||||
loop_pid = (None, None)
|
||||
|
||||
|
||||
_running_loop = _RunningLoop()
|
||||
|
||||
|
||||
@@ -633,7 +756,10 @@ def _get_running_loop():
|
||||
This is a low-level function intended to be used by event loops.
|
||||
This function is thread-specific.
|
||||
"""
|
||||
return _running_loop._loop
|
||||
# NOTE: this function is implemented in C (see _asynciomodule.c)
|
||||
running_loop, pid = _running_loop.loop_pid
|
||||
if running_loop is not None and pid == os.getpid():
|
||||
return running_loop
|
||||
|
||||
|
||||
def _set_running_loop(loop):
|
||||
@@ -642,7 +768,8 @@ def _set_running_loop(loop):
|
||||
This is a low-level function intended to be used by event loops.
|
||||
This function is thread-specific.
|
||||
"""
|
||||
_running_loop._loop = loop
|
||||
# NOTE: this function is implemented in C (see _asynciomodule.c)
|
||||
_running_loop.loop_pid = (loop, os.getpid())
|
||||
|
||||
|
||||
def _init_event_loop_policy():
|
||||
@@ -665,7 +792,8 @@ def set_event_loop_policy(policy):
|
||||
|
||||
If policy is None, the default policy is restored."""
|
||||
global _event_loop_policy
|
||||
assert policy is None or isinstance(policy, AbstractEventLoopPolicy)
|
||||
if policy is not None and not isinstance(policy, AbstractEventLoopPolicy):
|
||||
raise TypeError(f"policy must be an instance of AbstractEventLoopPolicy or None, not '{type(policy).__name__}'")
|
||||
_event_loop_policy = policy
|
||||
|
||||
|
||||
@@ -678,6 +806,7 @@ def get_event_loop():
|
||||
If there is no running event loop set, the function will return
|
||||
the result of `get_event_loop_policy().get_event_loop()` call.
|
||||
"""
|
||||
# NOTE: this function is implemented in C (see _asynciomodule.c)
|
||||
current_loop = _get_running_loop()
|
||||
if current_loop is not None:
|
||||
return current_loop
|
||||
@@ -703,3 +832,37 @@ def set_child_watcher(watcher):
|
||||
"""Equivalent to calling
|
||||
get_event_loop_policy().set_child_watcher(watcher)."""
|
||||
return get_event_loop_policy().set_child_watcher(watcher)
|
||||
|
||||
|
||||
# Alias pure-Python implementations for testing purposes.
|
||||
_py__get_running_loop = _get_running_loop
|
||||
_py__set_running_loop = _set_running_loop
|
||||
_py_get_running_loop = get_running_loop
|
||||
_py_get_event_loop = get_event_loop
|
||||
|
||||
|
||||
try:
|
||||
# get_event_loop() is one of the most frequently called
|
||||
# functions in asyncio. Pure Python implementation is
|
||||
# about 4 times slower than C-accelerated.
|
||||
from _asyncio import (_get_running_loop, _set_running_loop,
|
||||
get_running_loop, get_event_loop)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
# Alias C implementations for testing purposes.
|
||||
_c__get_running_loop = _get_running_loop
|
||||
_c__set_running_loop = _set_running_loop
|
||||
_c_get_running_loop = get_running_loop
|
||||
_c_get_event_loop = get_event_loop
|
||||
|
||||
|
||||
if hasattr(os, 'fork'):
|
||||
def on_fork():
|
||||
# Reset the loop and wakeupfd in the forked child process.
|
||||
if _event_loop_policy is not None:
|
||||
_event_loop_policy._local = BaseDefaultEventLoopPolicy._Local()
|
||||
_set_running_loop(None)
|
||||
signal.set_wakeup_fd(-1)
|
||||
|
||||
os.register_at_fork(after_in_child=on_fork)
|
||||
|
||||
62
Lib/asyncio/exceptions.py
vendored
Normal file
62
Lib/asyncio/exceptions.py
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
"""asyncio exceptions."""
|
||||
|
||||
|
||||
__all__ = ('BrokenBarrierError',
|
||||
'CancelledError', 'InvalidStateError', 'TimeoutError',
|
||||
'IncompleteReadError', 'LimitOverrunError',
|
||||
'SendfileNotAvailableError')
|
||||
|
||||
|
||||
class CancelledError(BaseException):
|
||||
"""The Future or Task was cancelled."""
|
||||
|
||||
|
||||
TimeoutError = TimeoutError # make local alias for the standard exception
|
||||
|
||||
|
||||
class InvalidStateError(Exception):
|
||||
"""The operation is not allowed in this state."""
|
||||
|
||||
|
||||
class SendfileNotAvailableError(RuntimeError):
|
||||
"""Sendfile syscall is not available.
|
||||
|
||||
Raised if OS does not support sendfile syscall for given socket or
|
||||
file type.
|
||||
"""
|
||||
|
||||
|
||||
class IncompleteReadError(EOFError):
|
||||
"""
|
||||
Incomplete read error. Attributes:
|
||||
|
||||
- partial: read bytes string before the end of stream was reached
|
||||
- expected: total number of expected bytes (or None if unknown)
|
||||
"""
|
||||
def __init__(self, partial, expected):
|
||||
r_expected = 'undefined' if expected is None else repr(expected)
|
||||
super().__init__(f'{len(partial)} bytes read on a total of '
|
||||
f'{r_expected} expected bytes')
|
||||
self.partial = partial
|
||||
self.expected = expected
|
||||
|
||||
def __reduce__(self):
|
||||
return type(self), (self.partial, self.expected)
|
||||
|
||||
|
||||
class LimitOverrunError(Exception):
|
||||
"""Reached the buffer limit while looking for a separator.
|
||||
|
||||
Attributes:
|
||||
- consumed: total number of to be consumed bytes.
|
||||
"""
|
||||
def __init__(self, message, consumed):
|
||||
super().__init__(message)
|
||||
self.consumed = consumed
|
||||
|
||||
def __reduce__(self):
|
||||
return type(self), (self.args[0], self.consumed)
|
||||
|
||||
|
||||
class BrokenBarrierError(RuntimeError):
|
||||
"""Barrier is broken by barrier.abort() call."""
|
||||
76
Lib/asyncio/format_helpers.py
vendored
Normal file
76
Lib/asyncio/format_helpers.py
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
import functools
|
||||
import inspect
|
||||
import reprlib
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from . import constants
|
||||
|
||||
|
||||
def _get_function_source(func):
|
||||
func = inspect.unwrap(func)
|
||||
if inspect.isfunction(func):
|
||||
code = func.__code__
|
||||
return (code.co_filename, code.co_firstlineno)
|
||||
if isinstance(func, functools.partial):
|
||||
return _get_function_source(func.func)
|
||||
if isinstance(func, functools.partialmethod):
|
||||
return _get_function_source(func.func)
|
||||
return None
|
||||
|
||||
|
||||
def _format_callback_source(func, args):
|
||||
func_repr = _format_callback(func, args, None)
|
||||
source = _get_function_source(func)
|
||||
if source:
|
||||
func_repr += f' at {source[0]}:{source[1]}'
|
||||
return func_repr
|
||||
|
||||
|
||||
def _format_args_and_kwargs(args, kwargs):
|
||||
"""Format function arguments and keyword arguments.
|
||||
|
||||
Special case for a single parameter: ('hello',) is formatted as ('hello').
|
||||
"""
|
||||
# use reprlib to limit the length of the output
|
||||
items = []
|
||||
if args:
|
||||
items.extend(reprlib.repr(arg) for arg in args)
|
||||
if kwargs:
|
||||
items.extend(f'{k}={reprlib.repr(v)}' for k, v in kwargs.items())
|
||||
return '({})'.format(', '.join(items))
|
||||
|
||||
|
||||
def _format_callback(func, args, kwargs, suffix=''):
|
||||
if isinstance(func, functools.partial):
|
||||
suffix = _format_args_and_kwargs(args, kwargs) + suffix
|
||||
return _format_callback(func.func, func.args, func.keywords, suffix)
|
||||
|
||||
if hasattr(func, '__qualname__') and func.__qualname__:
|
||||
func_repr = func.__qualname__
|
||||
elif hasattr(func, '__name__') and func.__name__:
|
||||
func_repr = func.__name__
|
||||
else:
|
||||
func_repr = repr(func)
|
||||
|
||||
func_repr += _format_args_and_kwargs(args, kwargs)
|
||||
if suffix:
|
||||
func_repr += suffix
|
||||
return func_repr
|
||||
|
||||
|
||||
def extract_stack(f=None, limit=None):
|
||||
"""Replacement for traceback.extract_stack() that only does the
|
||||
necessary work for asyncio debug mode.
|
||||
"""
|
||||
if f is None:
|
||||
f = sys._getframe().f_back
|
||||
if limit is None:
|
||||
# Limit the amount of work to a reasonable amount, as extract_stack()
|
||||
# can be called for each coroutine and future in debug mode.
|
||||
limit = constants.DEBUG_STACK_DEPTH
|
||||
stack = traceback.StackSummary.extract(traceback.walk_stack(f),
|
||||
limit=limit,
|
||||
lookup_lines=False)
|
||||
stack.reverse()
|
||||
return stack
|
||||
298
Lib/asyncio/futures.py
vendored
298
Lib/asyncio/futures.py
vendored
@@ -1,21 +1,21 @@
|
||||
"""A Future class similar to the one in PEP 3148."""
|
||||
|
||||
__all__ = ['CancelledError', 'TimeoutError', 'InvalidStateError',
|
||||
'Future', 'wrap_future', 'isfuture']
|
||||
__all__ = (
|
||||
'Future', 'wrap_future', 'isfuture',
|
||||
)
|
||||
|
||||
import concurrent.futures
|
||||
import contextvars
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from types import GenericAlias
|
||||
|
||||
from . import base_futures
|
||||
from . import compat
|
||||
from . import events
|
||||
from . import exceptions
|
||||
from . import format_helpers
|
||||
|
||||
|
||||
CancelledError = base_futures.CancelledError
|
||||
InvalidStateError = base_futures.InvalidStateError
|
||||
TimeoutError = base_futures.TimeoutError
|
||||
isfuture = base_futures.isfuture
|
||||
|
||||
|
||||
@@ -27,96 +27,18 @@ _FINISHED = base_futures._FINISHED
|
||||
STACK_DEBUG = logging.DEBUG - 1 # heavy-duty debugging
|
||||
|
||||
|
||||
class _TracebackLogger:
|
||||
"""Helper to log a traceback upon destruction if not cleared.
|
||||
|
||||
This solves a nasty problem with Futures and Tasks that have an
|
||||
exception set: if nobody asks for the exception, the exception is
|
||||
never logged. This violates the Zen of Python: 'Errors should
|
||||
never pass silently. Unless explicitly silenced.'
|
||||
|
||||
However, we don't want to log the exception as soon as
|
||||
set_exception() is called: if the calling code is written
|
||||
properly, it will get the exception and handle it properly. But
|
||||
we *do* want to log it if result() or exception() was never called
|
||||
-- otherwise developers waste a lot of time wondering why their
|
||||
buggy code fails silently.
|
||||
|
||||
An earlier attempt added a __del__() method to the Future class
|
||||
itself, but this backfired because the presence of __del__()
|
||||
prevents garbage collection from breaking cycles. A way out of
|
||||
this catch-22 is to avoid having a __del__() method on the Future
|
||||
class itself, but instead to have a reference to a helper object
|
||||
with a __del__() method that logs the traceback, where we ensure
|
||||
that the helper object doesn't participate in cycles, and only the
|
||||
Future has a reference to it.
|
||||
|
||||
The helper object is added when set_exception() is called. When
|
||||
the Future is collected, and the helper is present, the helper
|
||||
object is also collected, and its __del__() method will log the
|
||||
traceback. When the Future's result() or exception() method is
|
||||
called (and a helper object is present), it removes the helper
|
||||
object, after calling its clear() method to prevent it from
|
||||
logging.
|
||||
|
||||
One downside is that we do a fair amount of work to extract the
|
||||
traceback from the exception, even when it is never logged. It
|
||||
would seem cheaper to just store the exception object, but that
|
||||
references the traceback, which references stack frames, which may
|
||||
reference the Future, which references the _TracebackLogger, and
|
||||
then the _TracebackLogger would be included in a cycle, which is
|
||||
what we're trying to avoid! As an optimization, we don't
|
||||
immediately format the exception; we only do the work when
|
||||
activate() is called, which call is delayed until after all the
|
||||
Future's callbacks have run. Since usually a Future has at least
|
||||
one callback (typically set by 'yield from') and usually that
|
||||
callback extracts the callback, thereby removing the need to
|
||||
format the exception.
|
||||
|
||||
PS. I don't claim credit for this solution. I first heard of it
|
||||
in a discussion about closing files when they are collected.
|
||||
"""
|
||||
|
||||
__slots__ = ('loop', 'source_traceback', 'exc', 'tb')
|
||||
|
||||
def __init__(self, future, exc):
|
||||
self.loop = future._loop
|
||||
self.source_traceback = future._source_traceback
|
||||
self.exc = exc
|
||||
self.tb = None
|
||||
|
||||
def activate(self):
|
||||
exc = self.exc
|
||||
if exc is not None:
|
||||
self.exc = None
|
||||
self.tb = traceback.format_exception(exc.__class__, exc,
|
||||
exc.__traceback__)
|
||||
|
||||
def clear(self):
|
||||
self.exc = None
|
||||
self.tb = None
|
||||
|
||||
def __del__(self):
|
||||
if self.tb:
|
||||
msg = 'Future/Task exception was never retrieved\n'
|
||||
if self.source_traceback:
|
||||
src = ''.join(traceback.format_list(self.source_traceback))
|
||||
msg += 'Future/Task created at (most recent call last):\n'
|
||||
msg += '%s\n' % src.rstrip()
|
||||
msg += ''.join(self.tb).rstrip()
|
||||
self.loop.call_exception_handler({'message': msg})
|
||||
|
||||
|
||||
class Future:
|
||||
"""This class is *almost* compatible with concurrent.futures.Future.
|
||||
|
||||
Differences:
|
||||
|
||||
- This class is not thread-safe.
|
||||
|
||||
- result() and exception() do not take a timeout argument and
|
||||
raise an exception when the future isn't done yet.
|
||||
|
||||
- Callbacks registered with add_done_callback() are always called
|
||||
via the event loop's call_soon_threadsafe().
|
||||
via the event loop's call_soon().
|
||||
|
||||
- This class is not compatible with the wait() and as_completed()
|
||||
methods in the concurrent.futures package.
|
||||
@@ -130,6 +52,9 @@ class Future:
|
||||
_exception = None
|
||||
_loop = None
|
||||
_source_traceback = None
|
||||
_cancel_message = None
|
||||
# A saved CancelledError for later chaining as an exception context.
|
||||
_cancelled_exc = None
|
||||
|
||||
# This field is used for a dual purpose:
|
||||
# - Its presence is a marker to declare that a class implements
|
||||
@@ -137,12 +62,12 @@ class Future:
|
||||
# The value must also be not-None, to enable a subclass to declare
|
||||
# that it is not compatible by setting this to None.
|
||||
# - It is set by __iter__() below so that Task._step() can tell
|
||||
# the difference between `yield from Future()` (correct) vs.
|
||||
# the difference between
|
||||
# `await Future()` or`yield from Future()` (correct) vs.
|
||||
# `yield Future()` (incorrect).
|
||||
_asyncio_future_blocking = False
|
||||
|
||||
_log_traceback = False # Used for Python 3.4 and later
|
||||
_tb_logger = None # Used for Python 3.3 only
|
||||
__log_traceback = False
|
||||
|
||||
def __init__(self, *, loop=None):
|
||||
"""Initialize the future.
|
||||
@@ -157,50 +82,83 @@ class Future:
|
||||
self._loop = loop
|
||||
self._callbacks = []
|
||||
if self._loop.get_debug():
|
||||
self._source_traceback = traceback.extract_stack(sys._getframe(1))
|
||||
|
||||
_repr_info = base_futures._future_repr_info
|
||||
self._source_traceback = format_helpers.extract_stack(
|
||||
sys._getframe(1))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s>' % (self.__class__.__name__, ' '.join(self._repr_info()))
|
||||
return base_futures._future_repr(self)
|
||||
|
||||
# On Python 3.3 and older, objects with a destructor part of a reference
|
||||
# cycle are never destroyed. It's not more the case on Python 3.4 thanks
|
||||
# to the PEP 442.
|
||||
if compat.PY34:
|
||||
def __del__(self):
|
||||
if not self._log_traceback:
|
||||
# set_exception() was not called, or result() or exception()
|
||||
# has consumed the exception
|
||||
return
|
||||
exc = self._exception
|
||||
context = {
|
||||
'message': ('%s exception was never retrieved'
|
||||
% self.__class__.__name__),
|
||||
'exception': exc,
|
||||
'future': self,
|
||||
}
|
||||
if self._source_traceback:
|
||||
context['source_traceback'] = self._source_traceback
|
||||
self._loop.call_exception_handler(context)
|
||||
def __del__(self):
|
||||
if not self.__log_traceback:
|
||||
# set_exception() was not called, or result() or exception()
|
||||
# has consumed the exception
|
||||
return
|
||||
exc = self._exception
|
||||
context = {
|
||||
'message':
|
||||
f'{self.__class__.__name__} exception was never retrieved',
|
||||
'exception': exc,
|
||||
'future': self,
|
||||
}
|
||||
if self._source_traceback:
|
||||
context['source_traceback'] = self._source_traceback
|
||||
self._loop.call_exception_handler(context)
|
||||
|
||||
def __class_getitem__(cls, type):
|
||||
return cls
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
def cancel(self):
|
||||
@property
|
||||
def _log_traceback(self):
|
||||
return self.__log_traceback
|
||||
|
||||
@_log_traceback.setter
|
||||
def _log_traceback(self, val):
|
||||
if val:
|
||||
raise ValueError('_log_traceback can only be set to False')
|
||||
self.__log_traceback = False
|
||||
|
||||
def get_loop(self):
|
||||
"""Return the event loop the Future is bound to."""
|
||||
loop = self._loop
|
||||
if loop is None:
|
||||
raise RuntimeError("Future object is not initialized.")
|
||||
return loop
|
||||
|
||||
def _make_cancelled_error(self):
|
||||
"""Create the CancelledError to raise if the Future is cancelled.
|
||||
|
||||
This should only be called once when handling a cancellation since
|
||||
it erases the saved context exception value.
|
||||
"""
|
||||
if self._cancelled_exc is not None:
|
||||
exc = self._cancelled_exc
|
||||
self._cancelled_exc = None
|
||||
return exc
|
||||
|
||||
if self._cancel_message is None:
|
||||
exc = exceptions.CancelledError()
|
||||
else:
|
||||
exc = exceptions.CancelledError(self._cancel_message)
|
||||
exc.__context__ = self._cancelled_exc
|
||||
# Remove the reference since we don't need this anymore.
|
||||
self._cancelled_exc = None
|
||||
return exc
|
||||
|
||||
def cancel(self, msg=None):
|
||||
"""Cancel the future and schedule callbacks.
|
||||
|
||||
If the future is already done or cancelled, return False. Otherwise,
|
||||
change the future's state to cancelled, schedule the callbacks and
|
||||
return True.
|
||||
"""
|
||||
self.__log_traceback = False
|
||||
if self._state != _PENDING:
|
||||
return False
|
||||
self._state = _CANCELLED
|
||||
self._schedule_callbacks()
|
||||
self._cancel_message = msg
|
||||
self.__schedule_callbacks()
|
||||
return True
|
||||
|
||||
def _schedule_callbacks(self):
|
||||
def __schedule_callbacks(self):
|
||||
"""Internal: Ask the event loop to call all callbacks.
|
||||
|
||||
The callbacks are scheduled to be called as soon as possible. Also
|
||||
@@ -211,8 +169,8 @@ class Future:
|
||||
return
|
||||
|
||||
self._callbacks[:] = []
|
||||
for callback in callbacks:
|
||||
self._loop.call_soon(callback, self)
|
||||
for callback, ctx in callbacks:
|
||||
self._loop.call_soon(callback, self, context=ctx)
|
||||
|
||||
def cancelled(self):
|
||||
"""Return True if the future was cancelled."""
|
||||
@@ -236,15 +194,13 @@ class Future:
|
||||
the future is done and has an exception set, this exception is raised.
|
||||
"""
|
||||
if self._state == _CANCELLED:
|
||||
raise CancelledError
|
||||
exc = self._make_cancelled_error()
|
||||
raise exc
|
||||
if self._state != _FINISHED:
|
||||
raise InvalidStateError('Result is not ready.')
|
||||
self._log_traceback = False
|
||||
if self._tb_logger is not None:
|
||||
self._tb_logger.clear()
|
||||
self._tb_logger = None
|
||||
raise exceptions.InvalidStateError('Result is not ready.')
|
||||
self.__log_traceback = False
|
||||
if self._exception is not None:
|
||||
raise self._exception
|
||||
raise self._exception.with_traceback(self._exception_tb)
|
||||
return self._result
|
||||
|
||||
def exception(self):
|
||||
@@ -256,16 +212,14 @@ class Future:
|
||||
InvalidStateError.
|
||||
"""
|
||||
if self._state == _CANCELLED:
|
||||
raise CancelledError
|
||||
exc = self._make_cancelled_error()
|
||||
raise exc
|
||||
if self._state != _FINISHED:
|
||||
raise InvalidStateError('Exception is not set.')
|
||||
self._log_traceback = False
|
||||
if self._tb_logger is not None:
|
||||
self._tb_logger.clear()
|
||||
self._tb_logger = None
|
||||
raise exceptions.InvalidStateError('Exception is not set.')
|
||||
self.__log_traceback = False
|
||||
return self._exception
|
||||
|
||||
def add_done_callback(self, fn):
|
||||
def add_done_callback(self, fn, *, context=None):
|
||||
"""Add a callback to be run when the future becomes done.
|
||||
|
||||
The callback is called with a single argument - the future object. If
|
||||
@@ -273,9 +227,11 @@ class Future:
|
||||
scheduled with call_soon.
|
||||
"""
|
||||
if self._state != _PENDING:
|
||||
self._loop.call_soon(fn, self)
|
||||
self._loop.call_soon(fn, self, context=context)
|
||||
else:
|
||||
self._callbacks.append(fn)
|
||||
if context is None:
|
||||
context = contextvars.copy_context()
|
||||
self._callbacks.append((fn, context))
|
||||
|
||||
# New method not in PEP 3148.
|
||||
|
||||
@@ -284,7 +240,9 @@ class Future:
|
||||
|
||||
Returns the number of callbacks removed.
|
||||
"""
|
||||
filtered_callbacks = [f for f in self._callbacks if f != fn]
|
||||
filtered_callbacks = [(f, ctx)
|
||||
for (f, ctx) in self._callbacks
|
||||
if f != fn]
|
||||
removed_count = len(self._callbacks) - len(filtered_callbacks)
|
||||
if removed_count:
|
||||
self._callbacks[:] = filtered_callbacks
|
||||
@@ -299,10 +257,10 @@ class Future:
|
||||
InvalidStateError.
|
||||
"""
|
||||
if self._state != _PENDING:
|
||||
raise InvalidStateError('{}: {!r}'.format(self._state, self))
|
||||
raise exceptions.InvalidStateError(f'{self._state}: {self!r}')
|
||||
self._result = result
|
||||
self._state = _FINISHED
|
||||
self._schedule_callbacks()
|
||||
self.__schedule_callbacks()
|
||||
|
||||
def set_exception(self, exception):
|
||||
"""Mark the future done and set an exception.
|
||||
@@ -311,38 +269,45 @@ class Future:
|
||||
InvalidStateError.
|
||||
"""
|
||||
if self._state != _PENDING:
|
||||
raise InvalidStateError('{}: {!r}'.format(self._state, self))
|
||||
raise exceptions.InvalidStateError(f'{self._state}: {self!r}')
|
||||
if isinstance(exception, type):
|
||||
exception = exception()
|
||||
if type(exception) is StopIteration:
|
||||
raise TypeError("StopIteration interacts badly with generators "
|
||||
"and cannot be raised into a Future")
|
||||
self._exception = exception
|
||||
self._exception_tb = exception.__traceback__
|
||||
self._state = _FINISHED
|
||||
self._schedule_callbacks()
|
||||
if compat.PY34:
|
||||
self._log_traceback = True
|
||||
else:
|
||||
self._tb_logger = _TracebackLogger(self, exception)
|
||||
# Arrange for the logger to be activated after all callbacks
|
||||
# have had a chance to call result() or exception().
|
||||
self._loop.call_soon(self._tb_logger.activate)
|
||||
self.__schedule_callbacks()
|
||||
self.__log_traceback = True
|
||||
|
||||
def __iter__(self):
|
||||
def __await__(self):
|
||||
if not self.done():
|
||||
self._asyncio_future_blocking = True
|
||||
yield self # This tells Task to wait for completion.
|
||||
assert self.done(), "yield from wasn't used with future"
|
||||
if not self.done():
|
||||
raise RuntimeError("await wasn't used with future")
|
||||
return self.result() # May raise too.
|
||||
|
||||
if compat.PY35:
|
||||
__await__ = __iter__ # make compatible with 'await' expression
|
||||
__iter__ = __await__ # make compatible with 'yield from'.
|
||||
|
||||
|
||||
# Needed for testing purposes.
|
||||
_PyFuture = Future
|
||||
|
||||
|
||||
def _get_loop(fut):
|
||||
# Tries to call Future.get_loop() if it's available.
|
||||
# Otherwise fallbacks to using the old '_loop' property.
|
||||
try:
|
||||
get_loop = fut.get_loop
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
return get_loop()
|
||||
return fut._loop
|
||||
|
||||
|
||||
def _set_result_unless_cancelled(fut, result):
|
||||
"""Helper setting the result only if the future was not cancelled."""
|
||||
if fut.cancelled():
|
||||
@@ -350,6 +315,18 @@ def _set_result_unless_cancelled(fut, result):
|
||||
fut.set_result(result)
|
||||
|
||||
|
||||
def _convert_future_exc(exc):
|
||||
exc_class = type(exc)
|
||||
if exc_class is concurrent.futures.CancelledError:
|
||||
return exceptions.CancelledError(*exc.args)
|
||||
elif exc_class is concurrent.futures.TimeoutError:
|
||||
return exceptions.TimeoutError(*exc.args)
|
||||
elif exc_class is concurrent.futures.InvalidStateError:
|
||||
return exceptions.InvalidStateError(*exc.args)
|
||||
else:
|
||||
return exc
|
||||
|
||||
|
||||
def _set_concurrent_future_state(concurrent, source):
|
||||
"""Copy state from a future to a concurrent.futures.Future."""
|
||||
assert source.done()
|
||||
@@ -359,7 +336,7 @@ def _set_concurrent_future_state(concurrent, source):
|
||||
return
|
||||
exception = source.exception()
|
||||
if exception is not None:
|
||||
concurrent.set_exception(exception)
|
||||
concurrent.set_exception(_convert_future_exc(exception))
|
||||
else:
|
||||
result = source.result()
|
||||
concurrent.set_result(result)
|
||||
@@ -379,7 +356,7 @@ def _copy_future_state(source, dest):
|
||||
else:
|
||||
exception = source.exception()
|
||||
if exception is not None:
|
||||
dest.set_exception(exception)
|
||||
dest.set_exception(_convert_future_exc(exception))
|
||||
else:
|
||||
result = source.result()
|
||||
dest.set_result(result)
|
||||
@@ -398,8 +375,8 @@ def _chain_future(source, destination):
|
||||
if not isfuture(destination) and not isinstance(destination,
|
||||
concurrent.futures.Future):
|
||||
raise TypeError('A future is required for destination argument')
|
||||
source_loop = source._loop if isfuture(source) else None
|
||||
dest_loop = destination._loop if isfuture(destination) else None
|
||||
source_loop = _get_loop(source) if isfuture(source) else None
|
||||
dest_loop = _get_loop(destination) if isfuture(destination) else None
|
||||
|
||||
def _set_state(future, other):
|
||||
if isfuture(future):
|
||||
@@ -415,9 +392,14 @@ def _chain_future(source, destination):
|
||||
source_loop.call_soon_threadsafe(source.cancel)
|
||||
|
||||
def _call_set_state(source):
|
||||
if (destination.cancelled() and
|
||||
dest_loop is not None and dest_loop.is_closed()):
|
||||
return
|
||||
if dest_loop is None or dest_loop is source_loop:
|
||||
_set_state(destination, source)
|
||||
else:
|
||||
if dest_loop.is_closed():
|
||||
return
|
||||
dest_loop.call_soon_threadsafe(_set_state, destination, source)
|
||||
|
||||
destination.add_done_callback(_call_check_cancel)
|
||||
@@ -429,7 +411,7 @@ def wrap_future(future, *, loop=None):
|
||||
if isfuture(future):
|
||||
return future
|
||||
assert isinstance(future, concurrent.futures.Future), \
|
||||
'concurrent.futures.Future is expected, got {!r}'.format(future)
|
||||
f'concurrent.futures.Future is expected, got {future!r}'
|
||||
if loop is None:
|
||||
loop = events.get_event_loop()
|
||||
new_future = loop.create_future()
|
||||
|
||||
456
Lib/asyncio/locks.py
vendored
456
Lib/asyncio/locks.py
vendored
@@ -1,92 +1,26 @@
|
||||
"""Synchronization primitives."""
|
||||
|
||||
__all__ = ['Lock', 'Event', 'Condition', 'Semaphore', 'BoundedSemaphore']
|
||||
__all__ = ('Lock', 'Event', 'Condition', 'Semaphore',
|
||||
'BoundedSemaphore', 'Barrier')
|
||||
|
||||
import collections
|
||||
import enum
|
||||
|
||||
from . import compat
|
||||
from . import events
|
||||
from . import futures
|
||||
from .coroutines import coroutine
|
||||
from . import exceptions
|
||||
from . import mixins
|
||||
|
||||
|
||||
class _ContextManager:
|
||||
"""Context manager.
|
||||
|
||||
This enables the following idiom for acquiring and releasing a
|
||||
lock around a block:
|
||||
|
||||
with (yield from lock):
|
||||
<block>
|
||||
|
||||
while failing loudly when accidentally using:
|
||||
|
||||
with lock:
|
||||
<block>
|
||||
"""
|
||||
|
||||
def __init__(self, lock):
|
||||
self._lock = lock
|
||||
|
||||
def __enter__(self):
|
||||
class _ContextManagerMixin:
|
||||
async def __aenter__(self):
|
||||
await self.acquire()
|
||||
# We have no use for the "as ..." clause in the with
|
||||
# statement for locks.
|
||||
return None
|
||||
|
||||
def __exit__(self, *args):
|
||||
try:
|
||||
self._lock.release()
|
||||
finally:
|
||||
self._lock = None # Crudely prevent reuse.
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
self.release()
|
||||
|
||||
|
||||
class _ContextManagerMixin:
|
||||
def __enter__(self):
|
||||
raise RuntimeError(
|
||||
'"yield from" should be used as context manager expression')
|
||||
|
||||
def __exit__(self, *args):
|
||||
# This must exist because __enter__ exists, even though that
|
||||
# always raises; that's how the with-statement works.
|
||||
pass
|
||||
|
||||
@coroutine
|
||||
def __iter__(self):
|
||||
# This is not a coroutine. It is meant to enable the idiom:
|
||||
#
|
||||
# with (yield from lock):
|
||||
# <block>
|
||||
#
|
||||
# as an alternative to:
|
||||
#
|
||||
# yield from lock.acquire()
|
||||
# try:
|
||||
# <block>
|
||||
# finally:
|
||||
# lock.release()
|
||||
yield from self.acquire()
|
||||
return _ContextManager(self)
|
||||
|
||||
if compat.PY35:
|
||||
|
||||
def __await__(self):
|
||||
# To make "with await lock" work.
|
||||
yield from self.acquire()
|
||||
return _ContextManager(self)
|
||||
|
||||
@coroutine
|
||||
def __aenter__(self):
|
||||
yield from self.acquire()
|
||||
# We have no use for the "as ..." clause in the with
|
||||
# statement for locks.
|
||||
return None
|
||||
|
||||
@coroutine
|
||||
def __aexit__(self, exc_type, exc, tb):
|
||||
self.release()
|
||||
|
||||
|
||||
class Lock(_ContextManagerMixin):
|
||||
class Lock(_ContextManagerMixin, mixins._LoopBoundMixin):
|
||||
"""Primitive lock objects.
|
||||
|
||||
A primitive lock is a synchronization primitive that is not owned
|
||||
@@ -108,16 +42,16 @@ class Lock(_ContextManagerMixin):
|
||||
release() call resets the state to unlocked; first coroutine which
|
||||
is blocked in acquire() is being processed.
|
||||
|
||||
acquire() is a coroutine and should be called with 'yield from'.
|
||||
acquire() is a coroutine and should be called with 'await'.
|
||||
|
||||
Locks also support the context management protocol. '(yield from lock)'
|
||||
should be used as the context manager expression.
|
||||
Locks also support the asynchronous context management protocol.
|
||||
'async with lock' statement should be used.
|
||||
|
||||
Usage:
|
||||
|
||||
lock = Lock()
|
||||
...
|
||||
yield from lock
|
||||
await lock.acquire()
|
||||
try:
|
||||
...
|
||||
finally:
|
||||
@@ -127,57 +61,65 @@ class Lock(_ContextManagerMixin):
|
||||
|
||||
lock = Lock()
|
||||
...
|
||||
with (yield from lock):
|
||||
async with lock:
|
||||
...
|
||||
|
||||
Lock objects can be tested for locking state:
|
||||
|
||||
if not lock.locked():
|
||||
yield from lock
|
||||
await lock.acquire()
|
||||
else:
|
||||
# lock is acquired
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *, loop=None):
|
||||
self._waiters = collections.deque()
|
||||
def __init__(self):
|
||||
self._waiters = None
|
||||
self._locked = False
|
||||
if loop is not None:
|
||||
self._loop = loop
|
||||
else:
|
||||
self._loop = events.get_event_loop()
|
||||
|
||||
def __repr__(self):
|
||||
res = super().__repr__()
|
||||
extra = 'locked' if self._locked else 'unlocked'
|
||||
if self._waiters:
|
||||
extra = '{},waiters:{}'.format(extra, len(self._waiters))
|
||||
return '<{} [{}]>'.format(res[1:-1], extra)
|
||||
extra = f'{extra}, waiters:{len(self._waiters)}'
|
||||
return f'<{res[1:-1]} [{extra}]>'
|
||||
|
||||
def locked(self):
|
||||
"""Return True if lock is acquired."""
|
||||
return self._locked
|
||||
|
||||
@coroutine
|
||||
def acquire(self):
|
||||
async def acquire(self):
|
||||
"""Acquire a lock.
|
||||
|
||||
This method blocks until the lock is unlocked, then sets it to
|
||||
locked and returns True.
|
||||
"""
|
||||
if not self._locked and all(w.cancelled() for w in self._waiters):
|
||||
if (not self._locked and (self._waiters is None or
|
||||
all(w.cancelled() for w in self._waiters))):
|
||||
self._locked = True
|
||||
return True
|
||||
|
||||
fut = self._loop.create_future()
|
||||
if self._waiters is None:
|
||||
self._waiters = collections.deque()
|
||||
fut = self._get_loop().create_future()
|
||||
self._waiters.append(fut)
|
||||
|
||||
# Finally block should be called before the CancelledError
|
||||
# handling as we don't want CancelledError to call
|
||||
# _wake_up_first() and attempt to wake up itself.
|
||||
try:
|
||||
yield from fut
|
||||
self._locked = True
|
||||
return True
|
||||
finally:
|
||||
self._waiters.remove(fut)
|
||||
try:
|
||||
await fut
|
||||
finally:
|
||||
self._waiters.remove(fut)
|
||||
except exceptions.CancelledError:
|
||||
if not self._locked:
|
||||
self._wake_up_first()
|
||||
raise
|
||||
|
||||
self._locked = True
|
||||
return True
|
||||
|
||||
def release(self):
|
||||
"""Release a lock.
|
||||
@@ -192,16 +134,27 @@ class Lock(_ContextManagerMixin):
|
||||
"""
|
||||
if self._locked:
|
||||
self._locked = False
|
||||
# Wake up the first waiter who isn't cancelled.
|
||||
for fut in self._waiters:
|
||||
if not fut.done():
|
||||
fut.set_result(True)
|
||||
break
|
||||
self._wake_up_first()
|
||||
else:
|
||||
raise RuntimeError('Lock is not acquired.')
|
||||
|
||||
def _wake_up_first(self):
|
||||
"""Wake up the first waiter if it isn't done."""
|
||||
if not self._waiters:
|
||||
return
|
||||
try:
|
||||
fut = next(iter(self._waiters))
|
||||
except StopIteration:
|
||||
return
|
||||
|
||||
class Event:
|
||||
# .done() necessarily means that a waiter will wake up later on and
|
||||
# either take the lock, or, if it was cancelled and lock wasn't
|
||||
# taken already, will hit this again and wake up a new waiter.
|
||||
if not fut.done():
|
||||
fut.set_result(True)
|
||||
|
||||
|
||||
class Event(mixins._LoopBoundMixin):
|
||||
"""Asynchronous equivalent to threading.Event.
|
||||
|
||||
Class implementing event objects. An event manages a flag that can be set
|
||||
@@ -210,20 +163,16 @@ class Event:
|
||||
false.
|
||||
"""
|
||||
|
||||
def __init__(self, *, loop=None):
|
||||
def __init__(self):
|
||||
self._waiters = collections.deque()
|
||||
self._value = False
|
||||
if loop is not None:
|
||||
self._loop = loop
|
||||
else:
|
||||
self._loop = events.get_event_loop()
|
||||
|
||||
def __repr__(self):
|
||||
res = super().__repr__()
|
||||
extra = 'set' if self._value else 'unset'
|
||||
if self._waiters:
|
||||
extra = '{},waiters:{}'.format(extra, len(self._waiters))
|
||||
return '<{} [{}]>'.format(res[1:-1], extra)
|
||||
extra = f'{extra}, waiters:{len(self._waiters)}'
|
||||
return f'<{res[1:-1]} [{extra}]>'
|
||||
|
||||
def is_set(self):
|
||||
"""Return True if and only if the internal flag is true."""
|
||||
@@ -247,8 +196,7 @@ class Event:
|
||||
to true again."""
|
||||
self._value = False
|
||||
|
||||
@coroutine
|
||||
def wait(self):
|
||||
async def wait(self):
|
||||
"""Block until the internal flag is true.
|
||||
|
||||
If the internal flag is true on entry, return True
|
||||
@@ -258,16 +206,16 @@ class Event:
|
||||
if self._value:
|
||||
return True
|
||||
|
||||
fut = self._loop.create_future()
|
||||
fut = self._get_loop().create_future()
|
||||
self._waiters.append(fut)
|
||||
try:
|
||||
yield from fut
|
||||
await fut
|
||||
return True
|
||||
finally:
|
||||
self._waiters.remove(fut)
|
||||
|
||||
|
||||
class Condition(_ContextManagerMixin):
|
||||
class Condition(_ContextManagerMixin, mixins._LoopBoundMixin):
|
||||
"""Asynchronous equivalent to threading.Condition.
|
||||
|
||||
This class implements condition variable objects. A condition variable
|
||||
@@ -277,16 +225,9 @@ class Condition(_ContextManagerMixin):
|
||||
A new Lock object is created and used as the underlying lock.
|
||||
"""
|
||||
|
||||
def __init__(self, lock=None, *, loop=None):
|
||||
if loop is not None:
|
||||
self._loop = loop
|
||||
else:
|
||||
self._loop = events.get_event_loop()
|
||||
|
||||
def __init__(self, lock=None):
|
||||
if lock is None:
|
||||
lock = Lock(loop=self._loop)
|
||||
elif lock._loop is not self._loop:
|
||||
raise ValueError("loop argument must agree with lock")
|
||||
lock = Lock()
|
||||
|
||||
self._lock = lock
|
||||
# Export the lock's locked(), acquire() and release() methods.
|
||||
@@ -300,11 +241,10 @@ class Condition(_ContextManagerMixin):
|
||||
res = super().__repr__()
|
||||
extra = 'locked' if self.locked() else 'unlocked'
|
||||
if self._waiters:
|
||||
extra = '{},waiters:{}'.format(extra, len(self._waiters))
|
||||
return '<{} [{}]>'.format(res[1:-1], extra)
|
||||
extra = f'{extra}, waiters:{len(self._waiters)}'
|
||||
return f'<{res[1:-1]} [{extra}]>'
|
||||
|
||||
@coroutine
|
||||
def wait(self):
|
||||
async def wait(self):
|
||||
"""Wait until notified.
|
||||
|
||||
If the calling coroutine has not acquired the lock when this
|
||||
@@ -320,25 +260,28 @@ class Condition(_ContextManagerMixin):
|
||||
|
||||
self.release()
|
||||
try:
|
||||
fut = self._loop.create_future()
|
||||
fut = self._get_loop().create_future()
|
||||
self._waiters.append(fut)
|
||||
try:
|
||||
yield from fut
|
||||
await fut
|
||||
return True
|
||||
finally:
|
||||
self._waiters.remove(fut)
|
||||
|
||||
finally:
|
||||
# Must reacquire lock even if wait is cancelled
|
||||
cancelled = False
|
||||
while True:
|
||||
try:
|
||||
yield from self.acquire()
|
||||
await self.acquire()
|
||||
break
|
||||
except futures.CancelledError:
|
||||
pass
|
||||
except exceptions.CancelledError:
|
||||
cancelled = True
|
||||
|
||||
@coroutine
|
||||
def wait_for(self, predicate):
|
||||
if cancelled:
|
||||
raise exceptions.CancelledError
|
||||
|
||||
async def wait_for(self, predicate):
|
||||
"""Wait until a predicate becomes true.
|
||||
|
||||
The predicate should be a callable which result will be
|
||||
@@ -347,7 +290,7 @@ class Condition(_ContextManagerMixin):
|
||||
"""
|
||||
result = predicate()
|
||||
while not result:
|
||||
yield from self.wait()
|
||||
await self.wait()
|
||||
result = predicate()
|
||||
return result
|
||||
|
||||
@@ -384,7 +327,7 @@ class Condition(_ContextManagerMixin):
|
||||
self.notify(len(self._waiters))
|
||||
|
||||
|
||||
class Semaphore(_ContextManagerMixin):
|
||||
class Semaphore(_ContextManagerMixin, mixins._LoopBoundMixin):
|
||||
"""A Semaphore implementation.
|
||||
|
||||
A semaphore manages an internal counter which is decremented by each
|
||||
@@ -399,37 +342,25 @@ class Semaphore(_ContextManagerMixin):
|
||||
ValueError is raised.
|
||||
"""
|
||||
|
||||
def __init__(self, value=1, *, loop=None):
|
||||
def __init__(self, value=1):
|
||||
if value < 0:
|
||||
raise ValueError("Semaphore initial value must be >= 0")
|
||||
self._waiters = None
|
||||
self._value = value
|
||||
self._waiters = collections.deque()
|
||||
if loop is not None:
|
||||
self._loop = loop
|
||||
else:
|
||||
self._loop = events.get_event_loop()
|
||||
|
||||
def __repr__(self):
|
||||
res = super().__repr__()
|
||||
extra = 'locked' if self.locked() else 'unlocked,value:{}'.format(
|
||||
self._value)
|
||||
extra = 'locked' if self.locked() else f'unlocked, value:{self._value}'
|
||||
if self._waiters:
|
||||
extra = '{},waiters:{}'.format(extra, len(self._waiters))
|
||||
return '<{} [{}]>'.format(res[1:-1], extra)
|
||||
|
||||
def _wake_up_next(self):
|
||||
while self._waiters:
|
||||
waiter = self._waiters.popleft()
|
||||
if not waiter.done():
|
||||
waiter.set_result(None)
|
||||
return
|
||||
extra = f'{extra}, waiters:{len(self._waiters)}'
|
||||
return f'<{res[1:-1]} [{extra}]>'
|
||||
|
||||
def locked(self):
|
||||
"""Returns True if semaphore can not be acquired immediately."""
|
||||
return self._value == 0
|
||||
"""Returns True if semaphore cannot be acquired immediately."""
|
||||
return self._value == 0 or (
|
||||
any(not w.cancelled() for w in (self._waiters or ())))
|
||||
|
||||
@coroutine
|
||||
def acquire(self):
|
||||
async def acquire(self):
|
||||
"""Acquire a semaphore.
|
||||
|
||||
If the internal counter is larger than zero on entry,
|
||||
@@ -438,28 +369,53 @@ class Semaphore(_ContextManagerMixin):
|
||||
called release() to make it larger than 0, and then return
|
||||
True.
|
||||
"""
|
||||
while self._value <= 0:
|
||||
fut = self._loop.create_future()
|
||||
self._waiters.append(fut)
|
||||
if not self.locked():
|
||||
self._value -= 1
|
||||
return True
|
||||
|
||||
if self._waiters is None:
|
||||
self._waiters = collections.deque()
|
||||
fut = self._get_loop().create_future()
|
||||
self._waiters.append(fut)
|
||||
|
||||
# Finally block should be called before the CancelledError
|
||||
# handling as we don't want CancelledError to call
|
||||
# _wake_up_first() and attempt to wake up itself.
|
||||
try:
|
||||
try:
|
||||
yield from fut
|
||||
except:
|
||||
# See the similar code in Queue.get.
|
||||
fut.cancel()
|
||||
if self._value > 0 and not fut.cancelled():
|
||||
self._wake_up_next()
|
||||
raise
|
||||
self._value -= 1
|
||||
await fut
|
||||
finally:
|
||||
self._waiters.remove(fut)
|
||||
except exceptions.CancelledError:
|
||||
if not fut.cancelled():
|
||||
self._value += 1
|
||||
self._wake_up_next()
|
||||
raise
|
||||
|
||||
if self._value > 0:
|
||||
self._wake_up_next()
|
||||
return True
|
||||
|
||||
def release(self):
|
||||
"""Release a semaphore, incrementing the internal counter by one.
|
||||
|
||||
When it was zero on entry and another coroutine is waiting for it to
|
||||
become larger than zero again, wake up that coroutine.
|
||||
"""
|
||||
self._value += 1
|
||||
self._wake_up_next()
|
||||
|
||||
def _wake_up_next(self):
|
||||
"""Wake up the first waiter that isn't done."""
|
||||
if not self._waiters:
|
||||
return
|
||||
|
||||
for fut in self._waiters:
|
||||
if not fut.done():
|
||||
self._value -= 1
|
||||
fut.set_result(True)
|
||||
return
|
||||
|
||||
|
||||
class BoundedSemaphore(Semaphore):
|
||||
"""A bounded semaphore implementation.
|
||||
@@ -468,11 +424,163 @@ class BoundedSemaphore(Semaphore):
|
||||
above the initial value.
|
||||
"""
|
||||
|
||||
def __init__(self, value=1, *, loop=None):
|
||||
def __init__(self, value=1):
|
||||
self._bound_value = value
|
||||
super().__init__(value, loop=loop)
|
||||
super().__init__(value)
|
||||
|
||||
def release(self):
|
||||
if self._value >= self._bound_value:
|
||||
raise ValueError('BoundedSemaphore released too many times')
|
||||
super().release()
|
||||
|
||||
|
||||
|
||||
class _BarrierState(enum.Enum):
|
||||
FILLING = 'filling'
|
||||
DRAINING = 'draining'
|
||||
RESETTING = 'resetting'
|
||||
BROKEN = 'broken'
|
||||
|
||||
|
||||
class Barrier(mixins._LoopBoundMixin):
|
||||
"""Asyncio equivalent to threading.Barrier
|
||||
|
||||
Implements a Barrier primitive.
|
||||
Useful for synchronizing a fixed number of tasks at known synchronization
|
||||
points. Tasks block on 'wait()' and are simultaneously awoken once they
|
||||
have all made their call.
|
||||
"""
|
||||
|
||||
def __init__(self, parties):
|
||||
"""Create a barrier, initialised to 'parties' tasks."""
|
||||
if parties < 1:
|
||||
raise ValueError('parties must be > 0')
|
||||
|
||||
self._cond = Condition() # notify all tasks when state changes
|
||||
|
||||
self._parties = parties
|
||||
self._state = _BarrierState.FILLING
|
||||
self._count = 0 # count tasks in Barrier
|
||||
|
||||
def __repr__(self):
|
||||
res = super().__repr__()
|
||||
extra = f'{self._state.value}'
|
||||
if not self.broken:
|
||||
extra += f', waiters:{self.n_waiting}/{self.parties}'
|
||||
return f'<{res[1:-1]} [{extra}]>'
|
||||
|
||||
async def __aenter__(self):
|
||||
# wait for the barrier reaches the parties number
|
||||
# when start draining release and return index of waited task
|
||||
return await self.wait()
|
||||
|
||||
async def __aexit__(self, *args):
|
||||
pass
|
||||
|
||||
async def wait(self):
|
||||
"""Wait for the barrier.
|
||||
|
||||
When the specified number of tasks have started waiting, they are all
|
||||
simultaneously awoken.
|
||||
Returns an unique and individual index number from 0 to 'parties-1'.
|
||||
"""
|
||||
async with self._cond:
|
||||
await self._block() # Block while the barrier drains or resets.
|
||||
try:
|
||||
index = self._count
|
||||
self._count += 1
|
||||
if index + 1 == self._parties:
|
||||
# We release the barrier
|
||||
await self._release()
|
||||
else:
|
||||
await self._wait()
|
||||
return index
|
||||
finally:
|
||||
self._count -= 1
|
||||
# Wake up any tasks waiting for barrier to drain.
|
||||
self._exit()
|
||||
|
||||
async def _block(self):
|
||||
# Block until the barrier is ready for us,
|
||||
# or raise an exception if it is broken.
|
||||
#
|
||||
# It is draining or resetting, wait until done
|
||||
# unless a CancelledError occurs
|
||||
await self._cond.wait_for(
|
||||
lambda: self._state not in (
|
||||
_BarrierState.DRAINING, _BarrierState.RESETTING
|
||||
)
|
||||
)
|
||||
|
||||
# see if the barrier is in a broken state
|
||||
if self._state is _BarrierState.BROKEN:
|
||||
raise exceptions.BrokenBarrierError("Barrier aborted")
|
||||
|
||||
async def _release(self):
|
||||
# Release the tasks waiting in the barrier.
|
||||
|
||||
# Enter draining state.
|
||||
# Next waiting tasks will be blocked until the end of draining.
|
||||
self._state = _BarrierState.DRAINING
|
||||
self._cond.notify_all()
|
||||
|
||||
async def _wait(self):
|
||||
# Wait in the barrier until we are released. Raise an exception
|
||||
# if the barrier is reset or broken.
|
||||
|
||||
# wait for end of filling
|
||||
# unless a CancelledError occurs
|
||||
await self._cond.wait_for(lambda: self._state is not _BarrierState.FILLING)
|
||||
|
||||
if self._state in (_BarrierState.BROKEN, _BarrierState.RESETTING):
|
||||
raise exceptions.BrokenBarrierError("Abort or reset of barrier")
|
||||
|
||||
def _exit(self):
|
||||
# If we are the last tasks to exit the barrier, signal any tasks
|
||||
# waiting for the barrier to drain.
|
||||
if self._count == 0:
|
||||
if self._state in (_BarrierState.RESETTING, _BarrierState.DRAINING):
|
||||
self._state = _BarrierState.FILLING
|
||||
self._cond.notify_all()
|
||||
|
||||
async def reset(self):
|
||||
"""Reset the barrier to the initial state.
|
||||
|
||||
Any tasks currently waiting will get the BrokenBarrier exception
|
||||
raised.
|
||||
"""
|
||||
async with self._cond:
|
||||
if self._count > 0:
|
||||
if self._state is not _BarrierState.RESETTING:
|
||||
#reset the barrier, waking up tasks
|
||||
self._state = _BarrierState.RESETTING
|
||||
else:
|
||||
self._state = _BarrierState.FILLING
|
||||
self._cond.notify_all()
|
||||
|
||||
async def abort(self):
|
||||
"""Place the barrier into a 'broken' state.
|
||||
|
||||
Useful in case of error. Any currently waiting tasks and tasks
|
||||
attempting to 'wait()' will have BrokenBarrierError raised.
|
||||
"""
|
||||
async with self._cond:
|
||||
self._state = _BarrierState.BROKEN
|
||||
self._cond.notify_all()
|
||||
|
||||
@property
|
||||
def parties(self):
|
||||
"""Return the number of tasks required to trip the barrier."""
|
||||
return self._parties
|
||||
|
||||
@property
|
||||
def n_waiting(self):
|
||||
"""Return the number of tasks currently waiting at the barrier."""
|
||||
if self._state is _BarrierState.FILLING:
|
||||
return self._count
|
||||
return 0
|
||||
|
||||
@property
|
||||
def broken(self):
|
||||
"""Return True if the barrier is in a broken state."""
|
||||
return self._state is _BarrierState.BROKEN
|
||||
|
||||
21
Lib/asyncio/mixins.py
vendored
Normal file
21
Lib/asyncio/mixins.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Event loop mixins."""
|
||||
|
||||
import threading
|
||||
from . import events
|
||||
|
||||
_global_lock = threading.Lock()
|
||||
|
||||
|
||||
class _LoopBoundMixin:
|
||||
_loop = None
|
||||
|
||||
def _get_loop(self):
|
||||
loop = events._get_running_loop()
|
||||
|
||||
if self._loop is None:
|
||||
with _global_lock:
|
||||
if self._loop is None:
|
||||
self._loop = loop
|
||||
if loop is not self._loop:
|
||||
raise RuntimeError(f'{self!r} is bound to a different event loop')
|
||||
return loop
|
||||
563
Lib/asyncio/proactor_events.py
vendored
563
Lib/asyncio/proactor_events.py
vendored
@@ -4,20 +4,45 @@ A proactor is a "notify-on-completion" multiplexer. Currently a
|
||||
proactor is only implemented on Windows with IOCP.
|
||||
"""
|
||||
|
||||
__all__ = ['BaseProactorEventLoop']
|
||||
__all__ = 'BaseProactorEventLoop',
|
||||
|
||||
import io
|
||||
import os
|
||||
import socket
|
||||
import warnings
|
||||
import signal
|
||||
import threading
|
||||
import collections
|
||||
|
||||
from . import base_events
|
||||
from . import compat
|
||||
from . import constants
|
||||
from . import futures
|
||||
from . import exceptions
|
||||
from . import protocols
|
||||
from . import sslproto
|
||||
from . import transports
|
||||
from . import trsock
|
||||
from .log import logger
|
||||
|
||||
|
||||
def _set_socket_extra(transport, sock):
|
||||
transport._extra['socket'] = trsock.TransportSocket(sock)
|
||||
|
||||
try:
|
||||
transport._extra['sockname'] = sock.getsockname()
|
||||
except socket.error:
|
||||
if transport._loop.get_debug():
|
||||
logger.warning(
|
||||
"getsockname() failed on %r", sock, exc_info=True)
|
||||
|
||||
if 'peername' not in transport._extra:
|
||||
try:
|
||||
transport._extra['peername'] = sock.getpeername()
|
||||
except socket.error:
|
||||
# UDP sockets may not have a peer name
|
||||
transport._extra['peername'] = None
|
||||
|
||||
|
||||
class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
transports.BaseTransport):
|
||||
"""Base class for pipe and socket transports."""
|
||||
@@ -27,7 +52,7 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
super().__init__(extra, loop)
|
||||
self._set_extra(sock)
|
||||
self._sock = sock
|
||||
self._protocol = protocol
|
||||
self.set_protocol(protocol)
|
||||
self._server = server
|
||||
self._buffer = None # None or bytearray.
|
||||
self._read_fut = None
|
||||
@@ -35,6 +60,7 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
self._pending_write = 0
|
||||
self._conn_lost = 0
|
||||
self._closing = False # Set when close() called.
|
||||
self._called_connection_lost = False
|
||||
self._eof_written = False
|
||||
if self._server is not None:
|
||||
self._server._attach()
|
||||
@@ -51,17 +77,16 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
elif self._closing:
|
||||
info.append('closing')
|
||||
if self._sock is not None:
|
||||
info.append('fd=%s' % self._sock.fileno())
|
||||
info.append(f'fd={self._sock.fileno()}')
|
||||
if self._read_fut is not None:
|
||||
info.append('read=%s' % self._read_fut)
|
||||
info.append(f'read={self._read_fut!r}')
|
||||
if self._write_fut is not None:
|
||||
info.append("write=%r" % self._write_fut)
|
||||
info.append(f'write={self._write_fut!r}')
|
||||
if self._buffer:
|
||||
bufsize = len(self._buffer)
|
||||
info.append('write_bufsize=%s' % bufsize)
|
||||
info.append(f'write_bufsize={len(self._buffer)}')
|
||||
if self._eof_written:
|
||||
info.append('EOF written')
|
||||
return '<%s>' % ' '.join(info)
|
||||
return '<{}>'.format(' '.join(info))
|
||||
|
||||
def _set_extra(self, sock):
|
||||
self._extra['pipe'] = sock
|
||||
@@ -86,31 +111,33 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
self._read_fut.cancel()
|
||||
self._read_fut = None
|
||||
|
||||
# On Python 3.3 and older, objects with a destructor part of a reference
|
||||
# cycle are never destroyed. It's not more the case on Python 3.4 thanks
|
||||
# to the PEP 442.
|
||||
if compat.PY34:
|
||||
def __del__(self):
|
||||
if self._sock is not None:
|
||||
warnings.warn("unclosed transport %r" % self, ResourceWarning,
|
||||
source=self)
|
||||
self.close()
|
||||
def __del__(self, _warn=warnings.warn):
|
||||
if self._sock is not None:
|
||||
_warn(f"unclosed transport {self!r}", ResourceWarning, source=self)
|
||||
self._sock.close()
|
||||
|
||||
def _fatal_error(self, exc, message='Fatal error on pipe transport'):
|
||||
if isinstance(exc, base_events._FATAL_ERROR_IGNORE):
|
||||
if self._loop.get_debug():
|
||||
logger.debug("%r: %s", self, message, exc_info=True)
|
||||
else:
|
||||
self._loop.call_exception_handler({
|
||||
'message': message,
|
||||
'exception': exc,
|
||||
'transport': self,
|
||||
'protocol': self._protocol,
|
||||
})
|
||||
self._force_close(exc)
|
||||
try:
|
||||
if isinstance(exc, OSError):
|
||||
if self._loop.get_debug():
|
||||
logger.debug("%r: %s", self, message, exc_info=True)
|
||||
else:
|
||||
self._loop.call_exception_handler({
|
||||
'message': message,
|
||||
'exception': exc,
|
||||
'transport': self,
|
||||
'protocol': self._protocol,
|
||||
})
|
||||
finally:
|
||||
self._force_close(exc)
|
||||
|
||||
def _force_close(self, exc):
|
||||
if self._closing:
|
||||
if self._empty_waiter is not None and not self._empty_waiter.done():
|
||||
if exc is None:
|
||||
self._empty_waiter.set_result(None)
|
||||
else:
|
||||
self._empty_waiter.set_exception(exc)
|
||||
if self._closing and self._called_connection_lost:
|
||||
return
|
||||
self._closing = True
|
||||
self._conn_lost += 1
|
||||
@@ -125,6 +152,8 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
self._loop.call_soon(self._call_connection_lost, exc)
|
||||
|
||||
def _call_connection_lost(self, exc):
|
||||
if self._called_connection_lost:
|
||||
return
|
||||
try:
|
||||
self._protocol.connection_lost(exc)
|
||||
finally:
|
||||
@@ -132,7 +161,7 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
# end then it may fail with ERROR_NETNAME_DELETED if we
|
||||
# just close our end. First calling shutdown() seems to
|
||||
# cure it, but maybe using DisconnectEx() would be better.
|
||||
if hasattr(self._sock, 'shutdown'):
|
||||
if hasattr(self._sock, 'shutdown') and self._sock.fileno() != -1:
|
||||
self._sock.shutdown(socket.SHUT_RDWR)
|
||||
self._sock.close()
|
||||
self._sock = None
|
||||
@@ -140,6 +169,7 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
|
||||
if server is not None:
|
||||
server._detach()
|
||||
self._server = None
|
||||
self._called_connection_lost = True
|
||||
|
||||
def get_write_buffer_size(self):
|
||||
size = self._pending_write
|
||||
@@ -153,53 +183,127 @@ class _ProactorReadPipeTransport(_ProactorBasePipeTransport,
|
||||
"""Transport for read pipes."""
|
||||
|
||||
def __init__(self, loop, sock, protocol, waiter=None,
|
||||
extra=None, server=None):
|
||||
extra=None, server=None, buffer_size=65536):
|
||||
self._pending_data_length = -1
|
||||
self._paused = True
|
||||
super().__init__(loop, sock, protocol, waiter, extra, server)
|
||||
self._paused = False
|
||||
|
||||
self._data = bytearray(buffer_size)
|
||||
self._loop.call_soon(self._loop_reading)
|
||||
self._paused = False
|
||||
|
||||
def is_reading(self):
|
||||
return not self._paused and not self._closing
|
||||
|
||||
def pause_reading(self):
|
||||
if self._closing:
|
||||
raise RuntimeError('Cannot pause_reading() when closing')
|
||||
if self._paused:
|
||||
raise RuntimeError('Already paused')
|
||||
if self._closing or self._paused:
|
||||
return
|
||||
self._paused = True
|
||||
|
||||
# bpo-33694: Don't cancel self._read_fut because cancelling an
|
||||
# overlapped WSASend() loss silently data with the current proactor
|
||||
# implementation.
|
||||
#
|
||||
# If CancelIoEx() fails with ERROR_NOT_FOUND, it means that WSASend()
|
||||
# completed (even if HasOverlappedIoCompleted() returns 0), but
|
||||
# Overlapped.cancel() currently silently ignores the ERROR_NOT_FOUND
|
||||
# error. Once the overlapped is ignored, the IOCP loop will ignores the
|
||||
# completion I/O event and so not read the result of the overlapped
|
||||
# WSARecv().
|
||||
|
||||
if self._loop.get_debug():
|
||||
logger.debug("%r pauses reading", self)
|
||||
|
||||
def resume_reading(self):
|
||||
if not self._paused:
|
||||
raise RuntimeError('Not paused')
|
||||
self._paused = False
|
||||
if self._closing:
|
||||
if self._closing or not self._paused:
|
||||
return
|
||||
self._loop.call_soon(self._loop_reading, self._read_fut)
|
||||
|
||||
self._paused = False
|
||||
if self._read_fut is None:
|
||||
self._loop.call_soon(self._loop_reading, None)
|
||||
|
||||
length = self._pending_data_length
|
||||
self._pending_data_length = -1
|
||||
if length > -1:
|
||||
# Call the protocol method after calling _loop_reading(),
|
||||
# since the protocol can decide to pause reading again.
|
||||
self._loop.call_soon(self._data_received, self._data[:length], length)
|
||||
|
||||
if self._loop.get_debug():
|
||||
logger.debug("%r resumes reading", self)
|
||||
|
||||
def _loop_reading(self, fut=None):
|
||||
if self._paused:
|
||||
return
|
||||
data = None
|
||||
def _eof_received(self):
|
||||
if self._loop.get_debug():
|
||||
logger.debug("%r received EOF", self)
|
||||
|
||||
try:
|
||||
keep_open = self._protocol.eof_received()
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
self._fatal_error(
|
||||
exc, 'Fatal error: protocol.eof_received() call failed.')
|
||||
return
|
||||
|
||||
if not keep_open:
|
||||
self.close()
|
||||
|
||||
def _data_received(self, data, length):
|
||||
if self._paused:
|
||||
# Don't call any protocol method while reading is paused.
|
||||
# The protocol will be called on resume_reading().
|
||||
assert self._pending_data_length == -1
|
||||
self._pending_data_length = length
|
||||
return
|
||||
|
||||
if length == 0:
|
||||
self._eof_received()
|
||||
return
|
||||
|
||||
if isinstance(self._protocol, protocols.BufferedProtocol):
|
||||
try:
|
||||
protocols._feed_data_to_buffered_proto(self._protocol, data)
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
self._fatal_error(exc,
|
||||
'Fatal error: protocol.buffer_updated() '
|
||||
'call failed.')
|
||||
return
|
||||
else:
|
||||
self._protocol.data_received(data)
|
||||
|
||||
def _loop_reading(self, fut=None):
|
||||
length = -1
|
||||
data = None
|
||||
try:
|
||||
if fut is not None:
|
||||
assert self._read_fut is fut or (self._read_fut is None and
|
||||
self._closing)
|
||||
self._read_fut = None
|
||||
data = fut.result() # deliver data later in "finally" clause
|
||||
if fut.done():
|
||||
# deliver data later in "finally" clause
|
||||
length = fut.result()
|
||||
if length == 0:
|
||||
# we got end-of-file so no need to reschedule a new read
|
||||
return
|
||||
|
||||
# It's a new slice so make it immutable so protocols upstream don't have problems
|
||||
data = bytes(memoryview(self._data)[:length])
|
||||
else:
|
||||
# the future will be replaced by next proactor.recv call
|
||||
fut.cancel()
|
||||
|
||||
if self._closing:
|
||||
# since close() has been called we ignore any read data
|
||||
data = None
|
||||
return
|
||||
|
||||
if data == b'':
|
||||
# we got end-of-file so no need to reschedule a new read
|
||||
return
|
||||
# bpo-33694: buffer_updated() has currently no fast path because of
|
||||
# a data loss issue caused by overlapped WSASend() cancellation.
|
||||
|
||||
# reschedule a new read
|
||||
self._read_fut = self._loop._proactor.recv(self._sock, 4096)
|
||||
if not self._paused:
|
||||
# reschedule a new read
|
||||
self._read_fut = self._loop._proactor.recv_into(self._sock, self._data)
|
||||
except ConnectionAbortedError as exc:
|
||||
if not self._closing:
|
||||
self._fatal_error(exc, 'Fatal read error on pipe transport')
|
||||
@@ -210,32 +314,36 @@ class _ProactorReadPipeTransport(_ProactorBasePipeTransport,
|
||||
self._force_close(exc)
|
||||
except OSError as exc:
|
||||
self._fatal_error(exc, 'Fatal read error on pipe transport')
|
||||
except futures.CancelledError:
|
||||
except exceptions.CancelledError:
|
||||
if not self._closing:
|
||||
raise
|
||||
else:
|
||||
self._read_fut.add_done_callback(self._loop_reading)
|
||||
if not self._paused:
|
||||
self._read_fut.add_done_callback(self._loop_reading)
|
||||
finally:
|
||||
if data:
|
||||
self._protocol.data_received(data)
|
||||
elif data is not None:
|
||||
if self._loop.get_debug():
|
||||
logger.debug("%r received EOF", self)
|
||||
keep_open = self._protocol.eof_received()
|
||||
if not keep_open:
|
||||
self.close()
|
||||
if length > -1:
|
||||
self._data_received(data, length)
|
||||
|
||||
|
||||
class _ProactorBaseWritePipeTransport(_ProactorBasePipeTransport,
|
||||
transports.WriteTransport):
|
||||
"""Transport for write pipes."""
|
||||
|
||||
_start_tls_compatible = True
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super().__init__(*args, **kw)
|
||||
self._empty_waiter = None
|
||||
|
||||
def write(self, data):
|
||||
if not isinstance(data, (bytes, bytearray, memoryview)):
|
||||
raise TypeError('data argument must be byte-ish (%r)',
|
||||
type(data))
|
||||
raise TypeError(
|
||||
f"data argument must be a bytes-like object, "
|
||||
f"not {type(data).__name__}")
|
||||
if self._eof_written:
|
||||
raise RuntimeError('write_eof() already called')
|
||||
if self._empty_waiter is not None:
|
||||
raise RuntimeError('unable to write; sendfile is in progress')
|
||||
|
||||
if not data:
|
||||
return
|
||||
@@ -267,6 +375,10 @@ class _ProactorBaseWritePipeTransport(_ProactorBasePipeTransport,
|
||||
|
||||
def _loop_writing(self, f=None, data=None):
|
||||
try:
|
||||
if f is not None and self._write_fut is None and self._closing:
|
||||
# XXX most likely self._force_close() has been called, and
|
||||
# it has set self._write_fut to None.
|
||||
return
|
||||
assert f is self._write_fut
|
||||
self._write_fut = None
|
||||
self._pending_write = 0
|
||||
@@ -295,6 +407,8 @@ class _ProactorBaseWritePipeTransport(_ProactorBasePipeTransport,
|
||||
self._maybe_pause_protocol()
|
||||
else:
|
||||
self._write_fut.add_done_callback(self._loop_writing)
|
||||
if self._empty_waiter is not None and self._write_fut is None:
|
||||
self._empty_waiter.set_result(None)
|
||||
except ConnectionResetError as exc:
|
||||
self._force_close(exc)
|
||||
except OSError as exc:
|
||||
@@ -309,6 +423,17 @@ class _ProactorBaseWritePipeTransport(_ProactorBasePipeTransport,
|
||||
def abort(self):
|
||||
self._force_close(None)
|
||||
|
||||
def _make_empty_waiter(self):
|
||||
if self._empty_waiter is not None:
|
||||
raise RuntimeError("Empty waiter is already set")
|
||||
self._empty_waiter = self._loop.create_future()
|
||||
if self._write_fut is None:
|
||||
self._empty_waiter.set_result(None)
|
||||
return self._empty_waiter
|
||||
|
||||
def _reset_empty_waiter(self):
|
||||
self._empty_waiter = None
|
||||
|
||||
|
||||
class _ProactorWritePipeTransport(_ProactorBaseWritePipeTransport):
|
||||
def __init__(self, *args, **kw):
|
||||
@@ -332,6 +457,138 @@ class _ProactorWritePipeTransport(_ProactorBaseWritePipeTransport):
|
||||
self.close()
|
||||
|
||||
|
||||
class _ProactorDatagramTransport(_ProactorBasePipeTransport,
|
||||
transports.DatagramTransport):
|
||||
max_size = 256 * 1024
|
||||
def __init__(self, loop, sock, protocol, address=None,
|
||||
waiter=None, extra=None):
|
||||
self._address = address
|
||||
self._empty_waiter = None
|
||||
self._buffer_size = 0
|
||||
# We don't need to call _protocol.connection_made() since our base
|
||||
# constructor does it for us.
|
||||
super().__init__(loop, sock, protocol, waiter=waiter, extra=extra)
|
||||
|
||||
# The base constructor sets _buffer = None, so we set it here
|
||||
self._buffer = collections.deque()
|
||||
self._loop.call_soon(self._loop_reading)
|
||||
|
||||
def _set_extra(self, sock):
|
||||
_set_socket_extra(self, sock)
|
||||
|
||||
def get_write_buffer_size(self):
|
||||
return self._buffer_size
|
||||
|
||||
def abort(self):
|
||||
self._force_close(None)
|
||||
|
||||
def sendto(self, data, addr=None):
|
||||
if not isinstance(data, (bytes, bytearray, memoryview)):
|
||||
raise TypeError('data argument must be bytes-like object (%r)',
|
||||
type(data))
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if self._address is not None and addr not in (None, self._address):
|
||||
raise ValueError(
|
||||
f'Invalid address: must be None or {self._address}')
|
||||
|
||||
if self._conn_lost and self._address:
|
||||
if self._conn_lost >= constants.LOG_THRESHOLD_FOR_CONNLOST_WRITES:
|
||||
logger.warning('socket.sendto() raised exception.')
|
||||
self._conn_lost += 1
|
||||
return
|
||||
|
||||
# Ensure that what we buffer is immutable.
|
||||
self._buffer.append((bytes(data), addr))
|
||||
self._buffer_size += len(data)
|
||||
|
||||
if self._write_fut is None:
|
||||
# No current write operations are active, kick one off
|
||||
self._loop_writing()
|
||||
# else: A write operation is already kicked off
|
||||
|
||||
self._maybe_pause_protocol()
|
||||
|
||||
def _loop_writing(self, fut=None):
|
||||
try:
|
||||
if self._conn_lost:
|
||||
return
|
||||
|
||||
assert fut is self._write_fut
|
||||
self._write_fut = None
|
||||
if fut:
|
||||
# We are in a _loop_writing() done callback, get the result
|
||||
fut.result()
|
||||
|
||||
if not self._buffer or (self._conn_lost and self._address):
|
||||
# The connection has been closed
|
||||
if self._closing:
|
||||
self._loop.call_soon(self._call_connection_lost, None)
|
||||
return
|
||||
|
||||
data, addr = self._buffer.popleft()
|
||||
self._buffer_size -= len(data)
|
||||
if self._address is not None:
|
||||
self._write_fut = self._loop._proactor.send(self._sock,
|
||||
data)
|
||||
else:
|
||||
self._write_fut = self._loop._proactor.sendto(self._sock,
|
||||
data,
|
||||
addr=addr)
|
||||
except OSError as exc:
|
||||
self._protocol.error_received(exc)
|
||||
except Exception as exc:
|
||||
self._fatal_error(exc, 'Fatal write error on datagram transport')
|
||||
else:
|
||||
self._write_fut.add_done_callback(self._loop_writing)
|
||||
self._maybe_resume_protocol()
|
||||
|
||||
def _loop_reading(self, fut=None):
|
||||
data = None
|
||||
try:
|
||||
if self._conn_lost:
|
||||
return
|
||||
|
||||
assert self._read_fut is fut or (self._read_fut is None and
|
||||
self._closing)
|
||||
|
||||
self._read_fut = None
|
||||
if fut is not None:
|
||||
res = fut.result()
|
||||
|
||||
if self._closing:
|
||||
# since close() has been called we ignore any read data
|
||||
data = None
|
||||
return
|
||||
|
||||
if self._address is not None:
|
||||
data, addr = res, self._address
|
||||
else:
|
||||
data, addr = res
|
||||
|
||||
if self._conn_lost:
|
||||
return
|
||||
if self._address is not None:
|
||||
self._read_fut = self._loop._proactor.recv(self._sock,
|
||||
self.max_size)
|
||||
else:
|
||||
self._read_fut = self._loop._proactor.recvfrom(self._sock,
|
||||
self.max_size)
|
||||
except OSError as exc:
|
||||
self._protocol.error_received(exc)
|
||||
except exceptions.CancelledError:
|
||||
if not self._closing:
|
||||
raise
|
||||
else:
|
||||
if self._read_fut is not None:
|
||||
self._read_fut.add_done_callback(self._loop_reading)
|
||||
finally:
|
||||
if data:
|
||||
self._protocol.datagram_received(data, addr)
|
||||
|
||||
|
||||
class _ProactorDuplexPipeTransport(_ProactorReadPipeTransport,
|
||||
_ProactorBaseWritePipeTransport,
|
||||
transports.Transport):
|
||||
@@ -349,21 +606,15 @@ class _ProactorSocketTransport(_ProactorReadPipeTransport,
|
||||
transports.Transport):
|
||||
"""Transport for connected sockets."""
|
||||
|
||||
_sendfile_compatible = constants._SendfileMode.TRY_NATIVE
|
||||
|
||||
def __init__(self, loop, sock, protocol, waiter=None,
|
||||
extra=None, server=None):
|
||||
super().__init__(loop, sock, protocol, waiter, extra, server)
|
||||
base_events._set_nodelay(sock)
|
||||
|
||||
def _set_extra(self, sock):
|
||||
self._extra['socket'] = sock
|
||||
try:
|
||||
self._extra['sockname'] = sock.getsockname()
|
||||
except (socket.error, AttributeError):
|
||||
if self._loop.get_debug():
|
||||
logger.warning("getsockname() failed on %r",
|
||||
sock, exc_info=True)
|
||||
if 'peername' not in self._extra:
|
||||
try:
|
||||
self._extra['peername'] = sock.getpeername()
|
||||
except (socket.error, AttributeError):
|
||||
if self._loop.get_debug():
|
||||
logger.warning("getpeername() failed on %r",
|
||||
sock, exc_info=True)
|
||||
_set_socket_extra(self, sock)
|
||||
|
||||
def can_write_eof(self):
|
||||
return True
|
||||
@@ -387,26 +638,35 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
self._accept_futures = {} # socket file descriptor => Future
|
||||
proactor.set_loop(self)
|
||||
self._make_self_pipe()
|
||||
if threading.current_thread() is threading.main_thread():
|
||||
# wakeup fd can only be installed to a file descriptor from the main thread
|
||||
signal.set_wakeup_fd(self._csock.fileno())
|
||||
|
||||
def _make_socket_transport(self, sock, protocol, waiter=None,
|
||||
extra=None, server=None):
|
||||
return _ProactorSocketTransport(self, sock, protocol, waiter,
|
||||
extra, server)
|
||||
|
||||
def _make_ssl_transport(self, rawsock, protocol, sslcontext, waiter=None,
|
||||
*, server_side=False, server_hostname=None,
|
||||
extra=None, server=None):
|
||||
if not sslproto._is_sslproto_available():
|
||||
raise NotImplementedError("Proactor event loop requires Python 3.5"
|
||||
" or newer (ssl.MemoryBIO) to support "
|
||||
"SSL")
|
||||
|
||||
ssl_protocol = sslproto.SSLProtocol(self, protocol, sslcontext, waiter,
|
||||
server_side, server_hostname)
|
||||
def _make_ssl_transport(
|
||||
self, rawsock, protocol, sslcontext, waiter=None,
|
||||
*, server_side=False, server_hostname=None,
|
||||
extra=None, server=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None):
|
||||
ssl_protocol = sslproto.SSLProtocol(
|
||||
self, protocol, sslcontext, waiter,
|
||||
server_side, server_hostname,
|
||||
ssl_handshake_timeout=ssl_handshake_timeout,
|
||||
ssl_shutdown_timeout=ssl_shutdown_timeout)
|
||||
_ProactorSocketTransport(self, rawsock, ssl_protocol,
|
||||
extra=extra, server=server)
|
||||
return ssl_protocol._app_transport
|
||||
|
||||
def _make_datagram_transport(self, sock, protocol,
|
||||
address=None, waiter=None, extra=None):
|
||||
return _ProactorDatagramTransport(self, sock, protocol, address,
|
||||
waiter, extra)
|
||||
|
||||
def _make_duplex_pipe_transport(self, sock, protocol, waiter=None,
|
||||
extra=None):
|
||||
return _ProactorDuplexPipeTransport(self,
|
||||
@@ -428,6 +688,8 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
if self.is_closed():
|
||||
return
|
||||
|
||||
if threading.current_thread() is threading.main_thread():
|
||||
signal.set_wakeup_fd(-1)
|
||||
# Call these methods before closing the event loop (before calling
|
||||
# BaseEventLoop.close), because they can schedule callbacks with
|
||||
# call_soon(), which is forbidden when the event loop is closed.
|
||||
@@ -440,20 +702,73 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
# Close the event loop
|
||||
super().close()
|
||||
|
||||
def sock_recv(self, sock, n):
|
||||
return self._proactor.recv(sock, n)
|
||||
async def sock_recv(self, sock, n):
|
||||
return await self._proactor.recv(sock, n)
|
||||
|
||||
def sock_sendall(self, sock, data):
|
||||
return self._proactor.send(sock, data)
|
||||
async def sock_recv_into(self, sock, buf):
|
||||
return await self._proactor.recv_into(sock, buf)
|
||||
|
||||
def sock_connect(self, sock, address):
|
||||
return self._proactor.connect(sock, address)
|
||||
async def sock_recvfrom(self, sock, bufsize):
|
||||
return await self._proactor.recvfrom(sock, bufsize)
|
||||
|
||||
def sock_accept(self, sock):
|
||||
return self._proactor.accept(sock)
|
||||
async def sock_recvfrom_into(self, sock, buf, nbytes=0):
|
||||
if not nbytes:
|
||||
nbytes = len(buf)
|
||||
|
||||
def _socketpair(self):
|
||||
raise NotImplementedError
|
||||
return await self._proactor.recvfrom_into(sock, buf, nbytes)
|
||||
|
||||
async def sock_sendall(self, sock, data):
|
||||
return await self._proactor.send(sock, data)
|
||||
|
||||
async def sock_sendto(self, sock, data, address):
|
||||
return await self._proactor.sendto(sock, data, 0, address)
|
||||
|
||||
async def sock_connect(self, sock, address):
|
||||
return await self._proactor.connect(sock, address)
|
||||
|
||||
async def sock_accept(self, sock):
|
||||
return await self._proactor.accept(sock)
|
||||
|
||||
async def _sock_sendfile_native(self, sock, file, offset, count):
|
||||
try:
|
||||
fileno = file.fileno()
|
||||
except (AttributeError, io.UnsupportedOperation) as err:
|
||||
raise exceptions.SendfileNotAvailableError("not a regular file")
|
||||
try:
|
||||
fsize = os.fstat(fileno).st_size
|
||||
except OSError:
|
||||
raise exceptions.SendfileNotAvailableError("not a regular file")
|
||||
blocksize = count if count else fsize
|
||||
if not blocksize:
|
||||
return 0 # empty file
|
||||
|
||||
blocksize = min(blocksize, 0xffff_ffff)
|
||||
end_pos = min(offset + count, fsize) if count else fsize
|
||||
offset = min(offset, fsize)
|
||||
total_sent = 0
|
||||
try:
|
||||
while True:
|
||||
blocksize = min(end_pos - offset, blocksize)
|
||||
if blocksize <= 0:
|
||||
return total_sent
|
||||
await self._proactor.sendfile(sock, file, offset, blocksize)
|
||||
offset += blocksize
|
||||
total_sent += blocksize
|
||||
finally:
|
||||
if total_sent > 0:
|
||||
file.seek(offset)
|
||||
|
||||
async def _sendfile_native(self, transp, file, offset, count):
|
||||
resume_reading = transp.is_reading()
|
||||
transp.pause_reading()
|
||||
await transp._make_empty_waiter()
|
||||
try:
|
||||
return await self.sock_sendfile(transp._sock, file, offset, count,
|
||||
fallback=False)
|
||||
finally:
|
||||
transp._reset_empty_waiter()
|
||||
if resume_reading:
|
||||
transp.resume_reading()
|
||||
|
||||
def _close_self_pipe(self):
|
||||
if self._self_reading_future is not None:
|
||||
@@ -467,21 +782,30 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
|
||||
def _make_self_pipe(self):
|
||||
# A self-socket, really. :-)
|
||||
self._ssock, self._csock = self._socketpair()
|
||||
self._ssock, self._csock = socket.socketpair()
|
||||
self._ssock.setblocking(False)
|
||||
self._csock.setblocking(False)
|
||||
self._internal_fds += 1
|
||||
self.call_soon(self._loop_self_reading)
|
||||
|
||||
def _loop_self_reading(self, f=None):
|
||||
try:
|
||||
if f is not None:
|
||||
f.result() # may raise
|
||||
if self._self_reading_future is not f:
|
||||
# When we scheduled this Future, we assigned it to
|
||||
# _self_reading_future. If it's not there now, something has
|
||||
# tried to cancel the loop while this callback was still in the
|
||||
# queue (see windows_events.ProactorEventLoop.run_forever). In
|
||||
# that case stop here instead of continuing to schedule a new
|
||||
# iteration.
|
||||
return
|
||||
f = self._proactor.recv(self._ssock, 4096)
|
||||
except futures.CancelledError:
|
||||
except exceptions.CancelledError:
|
||||
# _close_self_pipe() has been called, stop waiting for data
|
||||
return
|
||||
except Exception as exc:
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
self.call_exception_handler({
|
||||
'message': 'Error on reading from the event loop self pipe',
|
||||
'exception': exc,
|
||||
@@ -492,10 +816,27 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
f.add_done_callback(self._loop_self_reading)
|
||||
|
||||
def _write_to_self(self):
|
||||
self._csock.send(b'\0')
|
||||
# This may be called from a different thread, possibly after
|
||||
# _close_self_pipe() has been called or even while it is
|
||||
# running. Guard for self._csock being None or closed. When
|
||||
# a socket is closed, send() raises OSError (with errno set to
|
||||
# EBADF, but let's not rely on the exact error code).
|
||||
csock = self._csock
|
||||
if csock is None:
|
||||
return
|
||||
|
||||
try:
|
||||
csock.send(b'\0')
|
||||
except OSError:
|
||||
if self._debug:
|
||||
logger.debug("Fail to write a null byte into the "
|
||||
"self-pipe socket",
|
||||
exc_info=True)
|
||||
|
||||
def _start_serving(self, protocol_factory, sock,
|
||||
sslcontext=None, server=None, backlog=100):
|
||||
sslcontext=None, server=None, backlog=100,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None):
|
||||
|
||||
def loop(f=None):
|
||||
try:
|
||||
@@ -508,7 +849,9 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
if sslcontext is not None:
|
||||
self._make_ssl_transport(
|
||||
conn, protocol, sslcontext, server_side=True,
|
||||
extra={'peername': addr}, server=server)
|
||||
extra={'peername': addr}, server=server,
|
||||
ssl_handshake_timeout=ssl_handshake_timeout,
|
||||
ssl_shutdown_timeout=ssl_shutdown_timeout)
|
||||
else:
|
||||
self._make_socket_transport(
|
||||
conn, protocol,
|
||||
@@ -521,13 +864,13 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
self.call_exception_handler({
|
||||
'message': 'Accept failed on a socket',
|
||||
'exception': exc,
|
||||
'socket': sock,
|
||||
'socket': trsock.TransportSocket(sock),
|
||||
})
|
||||
sock.close()
|
||||
elif self._debug:
|
||||
logger.debug("Accept failed on socket %r",
|
||||
sock, exc_info=True)
|
||||
except futures.CancelledError:
|
||||
except exceptions.CancelledError:
|
||||
sock.close()
|
||||
else:
|
||||
self._accept_futures[sock.fileno()] = f
|
||||
@@ -545,6 +888,8 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
|
||||
self._accept_futures.clear()
|
||||
|
||||
def _stop_serving(self, sock):
|
||||
self._stop_accept_futures()
|
||||
future = self._accept_futures.pop(sock.fileno(), None)
|
||||
if future:
|
||||
future.cancel()
|
||||
self._proactor._stop_serving(sock)
|
||||
sock.close()
|
||||
|
||||
88
Lib/asyncio/protocols.py
vendored
88
Lib/asyncio/protocols.py
vendored
@@ -1,7 +1,9 @@
|
||||
"""Abstract Protocol class."""
|
||||
"""Abstract Protocol base classes."""
|
||||
|
||||
__all__ = ['BaseProtocol', 'Protocol', 'DatagramProtocol',
|
||||
'SubprocessProtocol']
|
||||
__all__ = (
|
||||
'BaseProtocol', 'Protocol', 'DatagramProtocol',
|
||||
'SubprocessProtocol', 'BufferedProtocol',
|
||||
)
|
||||
|
||||
|
||||
class BaseProtocol:
|
||||
@@ -14,6 +16,8 @@ class BaseProtocol:
|
||||
write-only transport like write pipe
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Called when a connection is made.
|
||||
|
||||
@@ -85,6 +89,8 @@ class Protocol(BaseProtocol):
|
||||
* CL: connection_lost()
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def data_received(self, data):
|
||||
"""Called when some data is received.
|
||||
|
||||
@@ -100,9 +106,64 @@ class Protocol(BaseProtocol):
|
||||
"""
|
||||
|
||||
|
||||
class BufferedProtocol(BaseProtocol):
|
||||
"""Interface for stream protocol with manual buffer control.
|
||||
|
||||
Event methods, such as `create_server` and `create_connection`,
|
||||
accept factories that return protocols that implement this interface.
|
||||
|
||||
The idea of BufferedProtocol is that it allows to manually allocate
|
||||
and control the receive buffer. Event loops can then use the buffer
|
||||
provided by the protocol to avoid unnecessary data copies. This
|
||||
can result in noticeable performance improvement for protocols that
|
||||
receive big amounts of data. Sophisticated protocols can allocate
|
||||
the buffer only once at creation time.
|
||||
|
||||
State machine of calls:
|
||||
|
||||
start -> CM [-> GB [-> BU?]]* [-> ER?] -> CL -> end
|
||||
|
||||
* CM: connection_made()
|
||||
* GB: get_buffer()
|
||||
* BU: buffer_updated()
|
||||
* ER: eof_received()
|
||||
* CL: connection_lost()
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def get_buffer(self, sizehint):
|
||||
"""Called to allocate a new receive buffer.
|
||||
|
||||
*sizehint* is a recommended minimal size for the returned
|
||||
buffer. When set to -1, the buffer size can be arbitrary.
|
||||
|
||||
Must return an object that implements the
|
||||
:ref:`buffer protocol <bufferobjects>`.
|
||||
It is an error to return a zero-sized buffer.
|
||||
"""
|
||||
|
||||
def buffer_updated(self, nbytes):
|
||||
"""Called when the buffer was updated with the received data.
|
||||
|
||||
*nbytes* is the total number of bytes that were written to
|
||||
the buffer.
|
||||
"""
|
||||
|
||||
def eof_received(self):
|
||||
"""Called when the other end calls write_eof() or equivalent.
|
||||
|
||||
If this returns a false value (including None), the transport
|
||||
will close itself. If it returns a true value, closing the
|
||||
transport is up to the protocol.
|
||||
"""
|
||||
|
||||
|
||||
class DatagramProtocol(BaseProtocol):
|
||||
"""Interface for datagram protocol."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def datagram_received(self, data, addr):
|
||||
"""Called when some datagram is received."""
|
||||
|
||||
@@ -116,6 +177,8 @@ class DatagramProtocol(BaseProtocol):
|
||||
class SubprocessProtocol(BaseProtocol):
|
||||
"""Interface for protocol for subprocess calls."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def pipe_data_received(self, fd, data):
|
||||
"""Called when the subprocess writes data into stdout/stderr pipe.
|
||||
|
||||
@@ -132,3 +195,22 @@ class SubprocessProtocol(BaseProtocol):
|
||||
|
||||
def process_exited(self):
|
||||
"""Called when subprocess has exited."""
|
||||
|
||||
|
||||
def _feed_data_to_buffered_proto(proto, data):
|
||||
data_len = len(data)
|
||||
while data_len:
|
||||
buf = proto.get_buffer(data_len)
|
||||
buf_len = len(buf)
|
||||
if not buf_len:
|
||||
raise RuntimeError('get_buffer() returned an empty buffer')
|
||||
|
||||
if buf_len >= data_len:
|
||||
buf[:data_len] = data
|
||||
proto.buffer_updated(data_len)
|
||||
return
|
||||
else:
|
||||
buf[:buf_len] = data[:buf_len]
|
||||
proto.buffer_updated(buf_len)
|
||||
data = data[buf_len:]
|
||||
data_len = len(data)
|
||||
|
||||
90
Lib/asyncio/queues.py
vendored
90
Lib/asyncio/queues.py
vendored
@@ -1,35 +1,28 @@
|
||||
"""Queues"""
|
||||
|
||||
__all__ = ['Queue', 'PriorityQueue', 'LifoQueue', 'QueueFull', 'QueueEmpty']
|
||||
__all__ = ('Queue', 'PriorityQueue', 'LifoQueue', 'QueueFull', 'QueueEmpty')
|
||||
|
||||
import collections
|
||||
import heapq
|
||||
from types import GenericAlias
|
||||
|
||||
from . import compat
|
||||
from . import events
|
||||
from . import locks
|
||||
from .coroutines import coroutine
|
||||
from . import mixins
|
||||
|
||||
|
||||
class QueueEmpty(Exception):
|
||||
"""Exception raised when Queue.get_nowait() is called on a Queue object
|
||||
which is empty.
|
||||
"""
|
||||
"""Raised when Queue.get_nowait() is called on an empty Queue."""
|
||||
pass
|
||||
|
||||
|
||||
class QueueFull(Exception):
|
||||
"""Exception raised when the Queue.put_nowait() method is called on a Queue
|
||||
object which is full.
|
||||
"""
|
||||
"""Raised when the Queue.put_nowait() method is called on a full Queue."""
|
||||
pass
|
||||
|
||||
|
||||
class Queue:
|
||||
class Queue(mixins._LoopBoundMixin):
|
||||
"""A queue, useful for coordinating producer and consumer coroutines.
|
||||
|
||||
If maxsize is less than or equal to zero, the queue size is infinite. If it
|
||||
is an integer greater than 0, then "yield from put()" will block when the
|
||||
is an integer greater than 0, then "await put()" will block when the
|
||||
queue reaches maxsize, until an item is removed by get().
|
||||
|
||||
Unlike the standard library Queue, you can reliably know this Queue's size
|
||||
@@ -37,11 +30,7 @@ class Queue:
|
||||
interrupted between calling qsize() and doing an operation on the Queue.
|
||||
"""
|
||||
|
||||
def __init__(self, maxsize=0, *, loop=None):
|
||||
if loop is None:
|
||||
self._loop = events.get_event_loop()
|
||||
else:
|
||||
self._loop = loop
|
||||
def __init__(self, maxsize=0):
|
||||
self._maxsize = maxsize
|
||||
|
||||
# Futures.
|
||||
@@ -49,7 +38,7 @@ class Queue:
|
||||
# Futures.
|
||||
self._putters = collections.deque()
|
||||
self._unfinished_tasks = 0
|
||||
self._finished = locks.Event(loop=self._loop)
|
||||
self._finished = locks.Event()
|
||||
self._finished.set()
|
||||
self._init(maxsize)
|
||||
|
||||
@@ -75,25 +64,23 @@ class Queue:
|
||||
break
|
||||
|
||||
def __repr__(self):
|
||||
return '<{} at {:#x} {}>'.format(
|
||||
type(self).__name__, id(self), self._format())
|
||||
return f'<{type(self).__name__} at {id(self):#x} {self._format()}>'
|
||||
|
||||
def __str__(self):
|
||||
return '<{} {}>'.format(type(self).__name__, self._format())
|
||||
return f'<{type(self).__name__} {self._format()}>'
|
||||
|
||||
def __class_getitem__(cls, type):
|
||||
return cls
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
def _format(self):
|
||||
result = 'maxsize={!r}'.format(self._maxsize)
|
||||
result = f'maxsize={self._maxsize!r}'
|
||||
if getattr(self, '_queue', None):
|
||||
result += ' _queue={!r}'.format(list(self._queue))
|
||||
result += f' _queue={list(self._queue)!r}'
|
||||
if self._getters:
|
||||
result += ' _getters[{}]'.format(len(self._getters))
|
||||
result += f' _getters[{len(self._getters)}]'
|
||||
if self._putters:
|
||||
result += ' _putters[{}]'.format(len(self._putters))
|
||||
result += f' _putters[{len(self._putters)}]'
|
||||
if self._unfinished_tasks:
|
||||
result += ' tasks={}'.format(self._unfinished_tasks)
|
||||
result += f' tasks={self._unfinished_tasks}'
|
||||
return result
|
||||
|
||||
def qsize(self):
|
||||
@@ -120,22 +107,26 @@ class Queue:
|
||||
else:
|
||||
return self.qsize() >= self._maxsize
|
||||
|
||||
@coroutine
|
||||
def put(self, item):
|
||||
async def put(self, item):
|
||||
"""Put an item into the queue.
|
||||
|
||||
Put an item into the queue. If the queue is full, wait until a free
|
||||
slot is available before adding item.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
while self.full():
|
||||
putter = self._loop.create_future()
|
||||
putter = self._get_loop().create_future()
|
||||
self._putters.append(putter)
|
||||
try:
|
||||
yield from putter
|
||||
await putter
|
||||
except:
|
||||
putter.cancel() # Just in case putter is not done yet.
|
||||
try:
|
||||
# Clean self._putters from canceled putters.
|
||||
self._putters.remove(putter)
|
||||
except ValueError:
|
||||
# The putter could be removed from self._putters by a
|
||||
# previous get_nowait call.
|
||||
pass
|
||||
if not self.full() and not putter.cancelled():
|
||||
# We were woken up by get_nowait(), but can't take
|
||||
# the call. Wake up the next in line.
|
||||
@@ -155,21 +146,25 @@ class Queue:
|
||||
self._finished.clear()
|
||||
self._wakeup_next(self._getters)
|
||||
|
||||
@coroutine
|
||||
def get(self):
|
||||
async def get(self):
|
||||
"""Remove and return an item from the queue.
|
||||
|
||||
If queue is empty, wait until an item is available.
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
while self.empty():
|
||||
getter = self._loop.create_future()
|
||||
getter = self._get_loop().create_future()
|
||||
self._getters.append(getter)
|
||||
try:
|
||||
yield from getter
|
||||
await getter
|
||||
except:
|
||||
getter.cancel() # Just in case getter is not done yet.
|
||||
try:
|
||||
# Clean self._getters from canceled getters.
|
||||
self._getters.remove(getter)
|
||||
except ValueError:
|
||||
# The getter could be removed from self._getters by a
|
||||
# previous put_nowait call.
|
||||
pass
|
||||
if not self.empty() and not getter.cancelled():
|
||||
# We were woken up by put_nowait(), but can't take
|
||||
# the call. Wake up the next in line.
|
||||
@@ -208,8 +203,7 @@ class Queue:
|
||||
if self._unfinished_tasks == 0:
|
||||
self._finished.set()
|
||||
|
||||
@coroutine
|
||||
def join(self):
|
||||
async def join(self):
|
||||
"""Block until all items in the queue have been gotten and processed.
|
||||
|
||||
The count of unfinished tasks goes up whenever an item is added to the
|
||||
@@ -218,7 +212,7 @@ class Queue:
|
||||
When the count of unfinished tasks drops to zero, join() unblocks.
|
||||
"""
|
||||
if self._unfinished_tasks > 0:
|
||||
yield from self._finished.wait()
|
||||
await self._finished.wait()
|
||||
|
||||
|
||||
class PriorityQueue(Queue):
|
||||
@@ -248,9 +242,3 @@ class LifoQueue(Queue):
|
||||
|
||||
def _get(self):
|
||||
return self._queue.pop()
|
||||
|
||||
|
||||
if not compat.PY35:
|
||||
JoinableQueue = Queue
|
||||
"""Deprecated alias for Queue."""
|
||||
__all__.append('JoinableQueue')
|
||||
|
||||
187
Lib/asyncio/runners.py
vendored
187
Lib/asyncio/runners.py
vendored
@@ -1,16 +1,168 @@
|
||||
__all__ = ['run']
|
||||
__all__ = ('Runner', 'run')
|
||||
|
||||
import contextvars
|
||||
import enum
|
||||
import functools
|
||||
import threading
|
||||
import signal
|
||||
from . import coroutines
|
||||
from . import events
|
||||
from . import exceptions
|
||||
from . import tasks
|
||||
from . import constants
|
||||
|
||||
class _State(enum.Enum):
|
||||
CREATED = "created"
|
||||
INITIALIZED = "initialized"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
def run(main, *, debug=False):
|
||||
"""Run a coroutine.
|
||||
class Runner:
|
||||
"""A context manager that controls event loop life cycle.
|
||||
|
||||
The context manager always creates a new event loop,
|
||||
allows to run async functions inside it,
|
||||
and properly finalizes the loop at the context manager exit.
|
||||
|
||||
If debug is True, the event loop will be run in debug mode.
|
||||
If loop_factory is passed, it is used for new event loop creation.
|
||||
|
||||
asyncio.run(main(), debug=True)
|
||||
|
||||
is a shortcut for
|
||||
|
||||
with asyncio.Runner(debug=True) as runner:
|
||||
runner.run(main())
|
||||
|
||||
The run() method can be called multiple times within the runner's context.
|
||||
|
||||
This can be useful for interactive console (e.g. IPython),
|
||||
unittest runners, console tools, -- everywhere when async code
|
||||
is called from existing sync framework and where the preferred single
|
||||
asyncio.run() call doesn't work.
|
||||
|
||||
"""
|
||||
|
||||
# Note: the class is final, it is not intended for inheritance.
|
||||
|
||||
def __init__(self, *, debug=None, loop_factory=None):
|
||||
self._state = _State.CREATED
|
||||
self._debug = debug
|
||||
self._loop_factory = loop_factory
|
||||
self._loop = None
|
||||
self._context = None
|
||||
self._interrupt_count = 0
|
||||
self._set_event_loop = False
|
||||
|
||||
def __enter__(self):
|
||||
self._lazy_init()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
"""Shutdown and close event loop."""
|
||||
if self._state is not _State.INITIALIZED:
|
||||
return
|
||||
try:
|
||||
loop = self._loop
|
||||
_cancel_all_tasks(loop)
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.run_until_complete(
|
||||
loop.shutdown_default_executor(constants.THREAD_JOIN_TIMEOUT))
|
||||
finally:
|
||||
if self._set_event_loop:
|
||||
events.set_event_loop(None)
|
||||
loop.close()
|
||||
self._loop = None
|
||||
self._state = _State.CLOSED
|
||||
|
||||
def get_loop(self):
|
||||
"""Return embedded event loop."""
|
||||
self._lazy_init()
|
||||
return self._loop
|
||||
|
||||
def run(self, coro, *, context=None):
|
||||
"""Run a coroutine inside the embedded event loop."""
|
||||
if not coroutines.iscoroutine(coro):
|
||||
raise ValueError("a coroutine was expected, got {!r}".format(coro))
|
||||
|
||||
if events._get_running_loop() is not None:
|
||||
# fail fast with short traceback
|
||||
raise RuntimeError(
|
||||
"Runner.run() cannot be called from a running event loop")
|
||||
|
||||
self._lazy_init()
|
||||
|
||||
if context is None:
|
||||
context = self._context
|
||||
task = self._loop.create_task(coro, context=context)
|
||||
|
||||
if (threading.current_thread() is threading.main_thread()
|
||||
and signal.getsignal(signal.SIGINT) is signal.default_int_handler
|
||||
):
|
||||
sigint_handler = functools.partial(self._on_sigint, main_task=task)
|
||||
try:
|
||||
signal.signal(signal.SIGINT, sigint_handler)
|
||||
except ValueError:
|
||||
# `signal.signal` may throw if `threading.main_thread` does
|
||||
# not support signals (e.g. embedded interpreter with signals
|
||||
# not registered - see gh-91880)
|
||||
sigint_handler = None
|
||||
else:
|
||||
sigint_handler = None
|
||||
|
||||
self._interrupt_count = 0
|
||||
try:
|
||||
return self._loop.run_until_complete(task)
|
||||
except exceptions.CancelledError:
|
||||
if self._interrupt_count > 0:
|
||||
uncancel = getattr(task, "uncancel", None)
|
||||
if uncancel is not None and uncancel() == 0:
|
||||
raise KeyboardInterrupt()
|
||||
raise # CancelledError
|
||||
finally:
|
||||
if (sigint_handler is not None
|
||||
and signal.getsignal(signal.SIGINT) is sigint_handler
|
||||
):
|
||||
signal.signal(signal.SIGINT, signal.default_int_handler)
|
||||
|
||||
def _lazy_init(self):
|
||||
if self._state is _State.CLOSED:
|
||||
raise RuntimeError("Runner is closed")
|
||||
if self._state is _State.INITIALIZED:
|
||||
return
|
||||
if self._loop_factory is None:
|
||||
self._loop = events.new_event_loop()
|
||||
if not self._set_event_loop:
|
||||
# Call set_event_loop only once to avoid calling
|
||||
# attach_loop multiple times on child watchers
|
||||
events.set_event_loop(self._loop)
|
||||
self._set_event_loop = True
|
||||
else:
|
||||
self._loop = self._loop_factory()
|
||||
if self._debug is not None:
|
||||
self._loop.set_debug(self._debug)
|
||||
self._context = contextvars.copy_context()
|
||||
self._state = _State.INITIALIZED
|
||||
|
||||
def _on_sigint(self, signum, frame, main_task):
|
||||
self._interrupt_count += 1
|
||||
if self._interrupt_count == 1 and not main_task.done():
|
||||
main_task.cancel()
|
||||
# wakeup loop if it is blocked by select() with long timeout
|
||||
self._loop.call_soon_threadsafe(lambda: None)
|
||||
return
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
|
||||
def run(main, *, debug=None, loop_factory=None):
|
||||
"""Execute the coroutine and return the result.
|
||||
|
||||
This function runs the passed coroutine, taking care of
|
||||
managing the asyncio event loop and finalizing asynchronous
|
||||
generators.
|
||||
managing the asyncio event loop, finalizing asynchronous
|
||||
generators and closing the default executor.
|
||||
|
||||
This function cannot be called when another asyncio event loop is
|
||||
running in the same thread.
|
||||
@@ -21,6 +173,10 @@ def run(main, *, debug=False):
|
||||
It should be used as a main entry point for asyncio programs, and should
|
||||
ideally only be called once.
|
||||
|
||||
The executor is given a timeout duration of 5 minutes to shutdown.
|
||||
If the executor hasn't finished within that duration, a warning is
|
||||
emitted and the executor is closed.
|
||||
|
||||
Example:
|
||||
|
||||
async def main():
|
||||
@@ -30,24 +186,12 @@ def run(main, *, debug=False):
|
||||
asyncio.run(main())
|
||||
"""
|
||||
if events._get_running_loop() is not None:
|
||||
# fail fast with short traceback
|
||||
raise RuntimeError(
|
||||
"asyncio.run() cannot be called from a running event loop")
|
||||
|
||||
if not coroutines.iscoroutine(main):
|
||||
raise ValueError("a coroutine was expected, got {!r}".format(main))
|
||||
|
||||
loop = events.new_event_loop()
|
||||
try:
|
||||
events.set_event_loop(loop)
|
||||
loop.set_debug(debug)
|
||||
return loop.run_until_complete(main)
|
||||
finally:
|
||||
try:
|
||||
_cancel_all_tasks(loop)
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
finally:
|
||||
events.set_event_loop(None)
|
||||
loop.close()
|
||||
with Runner(debug=debug, loop_factory=loop_factory) as runner:
|
||||
return runner.run(main)
|
||||
|
||||
|
||||
def _cancel_all_tasks(loop):
|
||||
@@ -58,8 +202,7 @@ def _cancel_all_tasks(loop):
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
|
||||
loop.run_until_complete(
|
||||
tasks.gather(*to_cancel, loop=loop, return_exceptions=True))
|
||||
loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True))
|
||||
|
||||
for task in to_cancel:
|
||||
if task.cancelled():
|
||||
|
||||
1091
Lib/asyncio/selector_events.py
vendored
1091
Lib/asyncio/selector_events.py
vendored
File diff suppressed because it is too large
Load Diff
1136
Lib/asyncio/sslproto.py
vendored
1136
Lib/asyncio/sslproto.py
vendored
File diff suppressed because it is too large
Load Diff
149
Lib/asyncio/staggered.py
vendored
Normal file
149
Lib/asyncio/staggered.py
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
"""Support for running coroutines in parallel with staggered start times."""
|
||||
|
||||
__all__ = 'staggered_race',
|
||||
|
||||
import contextlib
|
||||
import typing
|
||||
|
||||
from . import events
|
||||
from . import exceptions as exceptions_mod
|
||||
from . import locks
|
||||
from . import tasks
|
||||
|
||||
|
||||
async def staggered_race(
|
||||
coro_fns: typing.Iterable[typing.Callable[[], typing.Awaitable]],
|
||||
delay: typing.Optional[float],
|
||||
*,
|
||||
loop: events.AbstractEventLoop = None,
|
||||
) -> typing.Tuple[
|
||||
typing.Any,
|
||||
typing.Optional[int],
|
||||
typing.List[typing.Optional[Exception]]
|
||||
]:
|
||||
"""Run coroutines with staggered start times and take the first to finish.
|
||||
|
||||
This method takes an iterable of coroutine functions. The first one is
|
||||
started immediately. From then on, whenever the immediately preceding one
|
||||
fails (raises an exception), or when *delay* seconds has passed, the next
|
||||
coroutine is started. This continues until one of the coroutines complete
|
||||
successfully, in which case all others are cancelled, or until all
|
||||
coroutines fail.
|
||||
|
||||
The coroutines provided should be well-behaved in the following way:
|
||||
|
||||
* They should only ``return`` if completed successfully.
|
||||
|
||||
* They should always raise an exception if they did not complete
|
||||
successfully. In particular, if they handle cancellation, they should
|
||||
probably reraise, like this::
|
||||
|
||||
try:
|
||||
# do work
|
||||
except asyncio.CancelledError:
|
||||
# undo partially completed work
|
||||
raise
|
||||
|
||||
Args:
|
||||
coro_fns: an iterable of coroutine functions, i.e. callables that
|
||||
return a coroutine object when called. Use ``functools.partial`` or
|
||||
lambdas to pass arguments.
|
||||
|
||||
delay: amount of time, in seconds, between starting coroutines. If
|
||||
``None``, the coroutines will run sequentially.
|
||||
|
||||
loop: the event loop to use.
|
||||
|
||||
Returns:
|
||||
tuple *(winner_result, winner_index, exceptions)* where
|
||||
|
||||
- *winner_result*: the result of the winning coroutine, or ``None``
|
||||
if no coroutines won.
|
||||
|
||||
- *winner_index*: the index of the winning coroutine in
|
||||
``coro_fns``, or ``None`` if no coroutines won. If the winning
|
||||
coroutine may return None on success, *winner_index* can be used
|
||||
to definitively determine whether any coroutine won.
|
||||
|
||||
- *exceptions*: list of exceptions returned by the coroutines.
|
||||
``len(exceptions)`` is equal to the number of coroutines actually
|
||||
started, and the order is the same as in ``coro_fns``. The winning
|
||||
coroutine's entry is ``None``.
|
||||
|
||||
"""
|
||||
# TODO: when we have aiter() and anext(), allow async iterables in coro_fns.
|
||||
loop = loop or events.get_running_loop()
|
||||
enum_coro_fns = enumerate(coro_fns)
|
||||
winner_result = None
|
||||
winner_index = None
|
||||
exceptions = []
|
||||
running_tasks = []
|
||||
|
||||
async def run_one_coro(
|
||||
previous_failed: typing.Optional[locks.Event]) -> None:
|
||||
# Wait for the previous task to finish, or for delay seconds
|
||||
if previous_failed is not None:
|
||||
with contextlib.suppress(exceptions_mod.TimeoutError):
|
||||
# Use asyncio.wait_for() instead of asyncio.wait() here, so
|
||||
# that if we get cancelled at this point, Event.wait() is also
|
||||
# cancelled, otherwise there will be a "Task destroyed but it is
|
||||
# pending" later.
|
||||
await tasks.wait_for(previous_failed.wait(), delay)
|
||||
# Get the next coroutine to run
|
||||
try:
|
||||
this_index, coro_fn = next(enum_coro_fns)
|
||||
except StopIteration:
|
||||
return
|
||||
# Start task that will run the next coroutine
|
||||
this_failed = locks.Event()
|
||||
next_task = loop.create_task(run_one_coro(this_failed))
|
||||
running_tasks.append(next_task)
|
||||
assert len(running_tasks) == this_index + 2
|
||||
# Prepare place to put this coroutine's exceptions if not won
|
||||
exceptions.append(None)
|
||||
assert len(exceptions) == this_index + 1
|
||||
|
||||
try:
|
||||
result = await coro_fn()
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as e:
|
||||
exceptions[this_index] = e
|
||||
this_failed.set() # Kickstart the next coroutine
|
||||
else:
|
||||
# Store winner's results
|
||||
nonlocal winner_index, winner_result
|
||||
assert winner_index is None
|
||||
winner_index = this_index
|
||||
winner_result = result
|
||||
# Cancel all other tasks. We take care to not cancel the current
|
||||
# task as well. If we do so, then since there is no `await` after
|
||||
# here and CancelledError are usually thrown at one, we will
|
||||
# encounter a curious corner case where the current task will end
|
||||
# up as done() == True, cancelled() == False, exception() ==
|
||||
# asyncio.CancelledError. This behavior is specified in
|
||||
# https://bugs.python.org/issue30048
|
||||
for i, t in enumerate(running_tasks):
|
||||
if i != this_index:
|
||||
t.cancel()
|
||||
|
||||
first_task = loop.create_task(run_one_coro(None))
|
||||
running_tasks.append(first_task)
|
||||
try:
|
||||
# Wait for a growing list of tasks to all finish: poor man's version of
|
||||
# curio's TaskGroup or trio's nursery
|
||||
done_count = 0
|
||||
while done_count != len(running_tasks):
|
||||
done, _ = await tasks.wait(running_tasks)
|
||||
done_count = len(done)
|
||||
# If run_one_coro raises an unhandled exception, it's probably a
|
||||
# programming error, and I want to see it.
|
||||
if __debug__:
|
||||
for d in done:
|
||||
if d.done() and not d.cancelled() and d.exception():
|
||||
raise d.exception()
|
||||
return winner_result, winner_index, exceptions
|
||||
finally:
|
||||
# Make sure no tasks are left running if we leave this function
|
||||
for t in running_tasks:
|
||||
t.cancel()
|
||||
411
Lib/asyncio/streams.py
vendored
411
Lib/asyncio/streams.py
vendored
@@ -1,55 +1,30 @@
|
||||
"""Stream-related things."""
|
||||
|
||||
__all__ = ['StreamReader', 'StreamWriter', 'StreamReaderProtocol',
|
||||
'open_connection', 'start_server',
|
||||
'IncompleteReadError',
|
||||
'LimitOverrunError',
|
||||
]
|
||||
__all__ = (
|
||||
'StreamReader', 'StreamWriter', 'StreamReaderProtocol',
|
||||
'open_connection', 'start_server')
|
||||
|
||||
import collections
|
||||
import socket
|
||||
import sys
|
||||
import warnings
|
||||
import weakref
|
||||
|
||||
if hasattr(socket, 'AF_UNIX'):
|
||||
__all__.extend(['open_unix_connection', 'start_unix_server'])
|
||||
__all__ += ('open_unix_connection', 'start_unix_server')
|
||||
|
||||
from . import coroutines
|
||||
from . import compat
|
||||
from . import events
|
||||
from . import exceptions
|
||||
from . import format_helpers
|
||||
from . import protocols
|
||||
from .coroutines import coroutine
|
||||
from .log import logger
|
||||
from .tasks import sleep
|
||||
|
||||
|
||||
_DEFAULT_LIMIT = 2 ** 16
|
||||
_DEFAULT_LIMIT = 2 ** 16 # 64 KiB
|
||||
|
||||
|
||||
class IncompleteReadError(EOFError):
|
||||
"""
|
||||
Incomplete read error. Attributes:
|
||||
|
||||
- partial: read bytes string before the end of stream was reached
|
||||
- expected: total number of expected bytes (or None if unknown)
|
||||
"""
|
||||
def __init__(self, partial, expected):
|
||||
super().__init__("%d bytes read on a total of %r expected bytes"
|
||||
% (len(partial), expected))
|
||||
self.partial = partial
|
||||
self.expected = expected
|
||||
|
||||
|
||||
class LimitOverrunError(Exception):
|
||||
"""Reached the buffer limit while looking for a separator.
|
||||
|
||||
Attributes:
|
||||
- consumed: total number of to be consumed bytes.
|
||||
"""
|
||||
def __init__(self, message, consumed):
|
||||
super().__init__(message)
|
||||
self.consumed = consumed
|
||||
|
||||
|
||||
@coroutine
|
||||
def open_connection(host=None, port=None, *,
|
||||
loop=None, limit=_DEFAULT_LIMIT, **kwds):
|
||||
async def open_connection(host=None, port=None, *,
|
||||
limit=_DEFAULT_LIMIT, **kwds):
|
||||
"""A wrapper for create_connection() returning a (reader, writer) pair.
|
||||
|
||||
The reader returned is a StreamReader instance; the writer is a
|
||||
@@ -67,19 +42,17 @@ def open_connection(host=None, port=None, *,
|
||||
StreamReaderProtocol classes, just copy the code -- there's
|
||||
really nothing special here except some convenience.)
|
||||
"""
|
||||
if loop is None:
|
||||
loop = events.get_event_loop()
|
||||
loop = events.get_running_loop()
|
||||
reader = StreamReader(limit=limit, loop=loop)
|
||||
protocol = StreamReaderProtocol(reader, loop=loop)
|
||||
transport, _ = yield from loop.create_connection(
|
||||
transport, _ = await loop.create_connection(
|
||||
lambda: protocol, host, port, **kwds)
|
||||
writer = StreamWriter(transport, protocol, reader, loop)
|
||||
return reader, writer
|
||||
|
||||
|
||||
@coroutine
|
||||
def start_server(client_connected_cb, host=None, port=None, *,
|
||||
loop=None, limit=_DEFAULT_LIMIT, **kwds):
|
||||
async def start_server(client_connected_cb, host=None, port=None, *,
|
||||
limit=_DEFAULT_LIMIT, **kwds):
|
||||
"""Start a socket server, call back for each client connected.
|
||||
|
||||
The first parameter, `client_connected_cb`, takes two parameters:
|
||||
@@ -94,15 +67,13 @@ def start_server(client_connected_cb, host=None, port=None, *,
|
||||
positional host and port, with various optional keyword arguments
|
||||
following. The return value is the same as loop.create_server().
|
||||
|
||||
Additional optional keyword arguments are loop (to set the event loop
|
||||
instance to use) and limit (to set the buffer limit passed to the
|
||||
StreamReader).
|
||||
Additional optional keyword argument is limit (to set the buffer
|
||||
limit passed to the StreamReader).
|
||||
|
||||
The return value is the same as loop.create_server(), i.e. a
|
||||
Server object which can be used to stop the service.
|
||||
"""
|
||||
if loop is None:
|
||||
loop = events.get_event_loop()
|
||||
loop = events.get_running_loop()
|
||||
|
||||
def factory():
|
||||
reader = StreamReader(limit=limit, loop=loop)
|
||||
@@ -110,31 +81,28 @@ def start_server(client_connected_cb, host=None, port=None, *,
|
||||
loop=loop)
|
||||
return protocol
|
||||
|
||||
return (yield from loop.create_server(factory, host, port, **kwds))
|
||||
return await loop.create_server(factory, host, port, **kwds)
|
||||
|
||||
|
||||
if hasattr(socket, 'AF_UNIX'):
|
||||
# UNIX Domain Sockets are supported on this platform
|
||||
|
||||
@coroutine
|
||||
def open_unix_connection(path=None, *,
|
||||
loop=None, limit=_DEFAULT_LIMIT, **kwds):
|
||||
async def open_unix_connection(path=None, *,
|
||||
limit=_DEFAULT_LIMIT, **kwds):
|
||||
"""Similar to `open_connection` but works with UNIX Domain Sockets."""
|
||||
if loop is None:
|
||||
loop = events.get_event_loop()
|
||||
loop = events.get_running_loop()
|
||||
|
||||
reader = StreamReader(limit=limit, loop=loop)
|
||||
protocol = StreamReaderProtocol(reader, loop=loop)
|
||||
transport, _ = yield from loop.create_unix_connection(
|
||||
transport, _ = await loop.create_unix_connection(
|
||||
lambda: protocol, path, **kwds)
|
||||
writer = StreamWriter(transport, protocol, reader, loop)
|
||||
return reader, writer
|
||||
|
||||
@coroutine
|
||||
def start_unix_server(client_connected_cb, path=None, *,
|
||||
loop=None, limit=_DEFAULT_LIMIT, **kwds):
|
||||
async def start_unix_server(client_connected_cb, path=None, *,
|
||||
limit=_DEFAULT_LIMIT, **kwds):
|
||||
"""Similar to `start_server` but works with UNIX Domain Sockets."""
|
||||
if loop is None:
|
||||
loop = events.get_event_loop()
|
||||
loop = events.get_running_loop()
|
||||
|
||||
def factory():
|
||||
reader = StreamReader(limit=limit, loop=loop)
|
||||
@@ -142,14 +110,14 @@ if hasattr(socket, 'AF_UNIX'):
|
||||
loop=loop)
|
||||
return protocol
|
||||
|
||||
return (yield from loop.create_unix_server(factory, path, **kwds))
|
||||
return await loop.create_unix_server(factory, path, **kwds)
|
||||
|
||||
|
||||
class FlowControlMixin(protocols.Protocol):
|
||||
"""Reusable flow control logic for StreamWriter.drain().
|
||||
|
||||
This implements the protocol methods pause_writing(),
|
||||
resume_reading() and connection_lost(). If the subclass overrides
|
||||
resume_writing() and connection_lost(). If the subclass overrides
|
||||
these it must call the super methods.
|
||||
|
||||
StreamWriter.drain() must wait for _drain_helper() coroutine.
|
||||
@@ -161,7 +129,7 @@ class FlowControlMixin(protocols.Protocol):
|
||||
else:
|
||||
self._loop = loop
|
||||
self._paused = False
|
||||
self._drain_waiter = None
|
||||
self._drain_waiters = collections.deque()
|
||||
self._connection_lost = False
|
||||
|
||||
def pause_writing(self):
|
||||
@@ -176,39 +144,37 @@ class FlowControlMixin(protocols.Protocol):
|
||||
if self._loop.get_debug():
|
||||
logger.debug("%r resumes writing", self)
|
||||
|
||||
waiter = self._drain_waiter
|
||||
if waiter is not None:
|
||||
self._drain_waiter = None
|
||||
for waiter in self._drain_waiters:
|
||||
if not waiter.done():
|
||||
waiter.set_result(None)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self._connection_lost = True
|
||||
# Wake up the writer if currently paused.
|
||||
# Wake up the writer(s) if currently paused.
|
||||
if not self._paused:
|
||||
return
|
||||
waiter = self._drain_waiter
|
||||
if waiter is None:
|
||||
return
|
||||
self._drain_waiter = None
|
||||
if waiter.done():
|
||||
return
|
||||
if exc is None:
|
||||
waiter.set_result(None)
|
||||
else:
|
||||
waiter.set_exception(exc)
|
||||
|
||||
@coroutine
|
||||
def _drain_helper(self):
|
||||
for waiter in self._drain_waiters:
|
||||
if not waiter.done():
|
||||
if exc is None:
|
||||
waiter.set_result(None)
|
||||
else:
|
||||
waiter.set_exception(exc)
|
||||
|
||||
async def _drain_helper(self):
|
||||
if self._connection_lost:
|
||||
raise ConnectionResetError('Connection lost')
|
||||
if not self._paused:
|
||||
return
|
||||
waiter = self._drain_waiter
|
||||
assert waiter is None or waiter.cancelled()
|
||||
waiter = self._loop.create_future()
|
||||
self._drain_waiter = waiter
|
||||
yield from waiter
|
||||
self._drain_waiters.append(waiter)
|
||||
try:
|
||||
await waiter
|
||||
finally:
|
||||
self._drain_waiters.remove(waiter)
|
||||
|
||||
def _get_close_waiter(self, stream):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class StreamReaderProtocol(FlowControlMixin, protocols.Protocol):
|
||||
@@ -220,40 +186,110 @@ class StreamReaderProtocol(FlowControlMixin, protocols.Protocol):
|
||||
call inappropriate methods of the protocol.)
|
||||
"""
|
||||
|
||||
_source_traceback = None
|
||||
|
||||
def __init__(self, stream_reader, client_connected_cb=None, loop=None):
|
||||
super().__init__(loop=loop)
|
||||
self._stream_reader = stream_reader
|
||||
if stream_reader is not None:
|
||||
self._stream_reader_wr = weakref.ref(stream_reader)
|
||||
self._source_traceback = stream_reader._source_traceback
|
||||
else:
|
||||
self._stream_reader_wr = None
|
||||
if client_connected_cb is not None:
|
||||
# This is a stream created by the `create_server()` function.
|
||||
# Keep a strong reference to the reader until a connection
|
||||
# is established.
|
||||
self._strong_reader = stream_reader
|
||||
self._reject_connection = False
|
||||
self._stream_writer = None
|
||||
self._task = None
|
||||
self._transport = None
|
||||
self._client_connected_cb = client_connected_cb
|
||||
self._over_ssl = False
|
||||
self._closed = self._loop.create_future()
|
||||
|
||||
@property
|
||||
def _stream_reader(self):
|
||||
if self._stream_reader_wr is None:
|
||||
return None
|
||||
return self._stream_reader_wr()
|
||||
|
||||
def _replace_writer(self, writer):
|
||||
loop = self._loop
|
||||
transport = writer.transport
|
||||
self._stream_writer = writer
|
||||
self._transport = transport
|
||||
self._over_ssl = transport.get_extra_info('sslcontext') is not None
|
||||
|
||||
def connection_made(self, transport):
|
||||
self._stream_reader.set_transport(transport)
|
||||
if self._reject_connection:
|
||||
context = {
|
||||
'message': ('An open stream was garbage collected prior to '
|
||||
'establishing network connection; '
|
||||
'call "stream.close()" explicitly.')
|
||||
}
|
||||
if self._source_traceback:
|
||||
context['source_traceback'] = self._source_traceback
|
||||
self._loop.call_exception_handler(context)
|
||||
transport.abort()
|
||||
return
|
||||
self._transport = transport
|
||||
reader = self._stream_reader
|
||||
if reader is not None:
|
||||
reader.set_transport(transport)
|
||||
self._over_ssl = transport.get_extra_info('sslcontext') is not None
|
||||
if self._client_connected_cb is not None:
|
||||
self._stream_writer = StreamWriter(transport, self,
|
||||
self._stream_reader,
|
||||
reader,
|
||||
self._loop)
|
||||
res = self._client_connected_cb(self._stream_reader,
|
||||
res = self._client_connected_cb(reader,
|
||||
self._stream_writer)
|
||||
if coroutines.iscoroutine(res):
|
||||
self._loop.create_task(res)
|
||||
def callback(task):
|
||||
if task.cancelled():
|
||||
transport.close()
|
||||
return
|
||||
exc = task.exception()
|
||||
if exc is not None:
|
||||
self._loop.call_exception_handler({
|
||||
'message': 'Unhandled exception in client_connected_cb',
|
||||
'exception': exc,
|
||||
'transport': transport,
|
||||
})
|
||||
transport.close()
|
||||
|
||||
self._task = self._loop.create_task(res)
|
||||
self._task.add_done_callback(callback)
|
||||
|
||||
self._strong_reader = None
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if self._stream_reader is not None:
|
||||
reader = self._stream_reader
|
||||
if reader is not None:
|
||||
if exc is None:
|
||||
self._stream_reader.feed_eof()
|
||||
reader.feed_eof()
|
||||
else:
|
||||
self._stream_reader.set_exception(exc)
|
||||
reader.set_exception(exc)
|
||||
if not self._closed.done():
|
||||
if exc is None:
|
||||
self._closed.set_result(None)
|
||||
else:
|
||||
self._closed.set_exception(exc)
|
||||
super().connection_lost(exc)
|
||||
self._stream_reader = None
|
||||
self._stream_reader_wr = None
|
||||
self._stream_writer = None
|
||||
self._task = None
|
||||
self._transport = None
|
||||
|
||||
def data_received(self, data):
|
||||
self._stream_reader.feed_data(data)
|
||||
reader = self._stream_reader
|
||||
if reader is not None:
|
||||
reader.feed_data(data)
|
||||
|
||||
def eof_received(self):
|
||||
self._stream_reader.feed_eof()
|
||||
reader = self._stream_reader
|
||||
if reader is not None:
|
||||
reader.feed_eof()
|
||||
if self._over_ssl:
|
||||
# Prevent a warning in SSLProtocol.eof_received:
|
||||
# "returning true from eof_received()
|
||||
@@ -261,6 +297,20 @@ class StreamReaderProtocol(FlowControlMixin, protocols.Protocol):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_close_waiter(self, stream):
|
||||
return self._closed
|
||||
|
||||
def __del__(self):
|
||||
# Prevent reports about unhandled exceptions.
|
||||
# Better than self._closed._log_traceback = False hack
|
||||
try:
|
||||
closed = self._closed
|
||||
except AttributeError:
|
||||
pass # failed constructor
|
||||
else:
|
||||
if closed.done() and not closed.cancelled():
|
||||
closed.exception()
|
||||
|
||||
|
||||
class StreamWriter:
|
||||
"""Wraps a Transport.
|
||||
@@ -279,12 +329,14 @@ class StreamWriter:
|
||||
assert reader is None or isinstance(reader, StreamReader)
|
||||
self._reader = reader
|
||||
self._loop = loop
|
||||
self._complete_fut = self._loop.create_future()
|
||||
self._complete_fut.set_result(None)
|
||||
|
||||
def __repr__(self):
|
||||
info = [self.__class__.__name__, 'transport=%r' % self._transport]
|
||||
info = [self.__class__.__name__, f'transport={self._transport!r}']
|
||||
if self._reader is not None:
|
||||
info.append('reader=%r' % self._reader)
|
||||
return '<%s>' % ' '.join(info)
|
||||
info.append(f'reader={self._reader!r}')
|
||||
return '<{}>'.format(' '.join(info))
|
||||
|
||||
@property
|
||||
def transport(self):
|
||||
@@ -305,36 +357,68 @@ class StreamWriter:
|
||||
def close(self):
|
||||
return self._transport.close()
|
||||
|
||||
def is_closing(self):
|
||||
return self._transport.is_closing()
|
||||
|
||||
async def wait_closed(self):
|
||||
await self._protocol._get_close_waiter(self)
|
||||
|
||||
def get_extra_info(self, name, default=None):
|
||||
return self._transport.get_extra_info(name, default)
|
||||
|
||||
@coroutine
|
||||
def drain(self):
|
||||
async def drain(self):
|
||||
"""Flush the write buffer.
|
||||
|
||||
The intended use is to write
|
||||
|
||||
w.write(data)
|
||||
yield from w.drain()
|
||||
await w.drain()
|
||||
"""
|
||||
if self._reader is not None:
|
||||
exc = self._reader.exception()
|
||||
if exc is not None:
|
||||
raise exc
|
||||
if self._transport is not None:
|
||||
if self._transport.is_closing():
|
||||
# Yield to the event loop so connection_lost() may be
|
||||
# called. Without this, _drain_helper() would return
|
||||
# immediately, and code that calls
|
||||
# write(...); yield from drain()
|
||||
# in a loop would never call connection_lost(), so it
|
||||
# would not see an error when the socket is closed.
|
||||
yield
|
||||
yield from self._protocol._drain_helper()
|
||||
if self._transport.is_closing():
|
||||
# Wait for protocol.connection_lost() call
|
||||
# Raise connection closing error if any,
|
||||
# ConnectionResetError otherwise
|
||||
# Yield to the event loop so connection_lost() may be
|
||||
# called. Without this, _drain_helper() would return
|
||||
# immediately, and code that calls
|
||||
# write(...); await drain()
|
||||
# in a loop would never call connection_lost(), so it
|
||||
# would not see an error when the socket is closed.
|
||||
await sleep(0)
|
||||
await self._protocol._drain_helper()
|
||||
|
||||
async def start_tls(self, sslcontext, *,
|
||||
server_hostname=None,
|
||||
ssl_handshake_timeout=None,
|
||||
ssl_shutdown_timeout=None):
|
||||
"""Upgrade an existing stream-based connection to TLS."""
|
||||
server_side = self._protocol._client_connected_cb is not None
|
||||
protocol = self._protocol
|
||||
await self.drain()
|
||||
new_transport = await self._loop.start_tls( # type: ignore
|
||||
self._transport, protocol, sslcontext,
|
||||
server_side=server_side, server_hostname=server_hostname,
|
||||
ssl_handshake_timeout=ssl_handshake_timeout,
|
||||
ssl_shutdown_timeout=ssl_shutdown_timeout)
|
||||
self._transport = new_transport
|
||||
protocol._replace_writer(self)
|
||||
|
||||
def __del__(self):
|
||||
if not self._transport.is_closing():
|
||||
if self._loop.is_closed():
|
||||
warnings.warn("loop is closed", ResourceWarning)
|
||||
else:
|
||||
self.close()
|
||||
warnings.warn(f"unclosed {self!r}", ResourceWarning)
|
||||
|
||||
class StreamReader:
|
||||
|
||||
_source_traceback = None
|
||||
|
||||
def __init__(self, limit=_DEFAULT_LIMIT, loop=None):
|
||||
# The line length limit is a security feature;
|
||||
# it also doubles as half the buffer limit.
|
||||
@@ -353,24 +437,27 @@ class StreamReader:
|
||||
self._exception = None
|
||||
self._transport = None
|
||||
self._paused = False
|
||||
if self._loop.get_debug():
|
||||
self._source_traceback = format_helpers.extract_stack(
|
||||
sys._getframe(1))
|
||||
|
||||
def __repr__(self):
|
||||
info = ['StreamReader']
|
||||
if self._buffer:
|
||||
info.append('%d bytes' % len(self._buffer))
|
||||
info.append(f'{len(self._buffer)} bytes')
|
||||
if self._eof:
|
||||
info.append('eof')
|
||||
if self._limit != _DEFAULT_LIMIT:
|
||||
info.append('l=%d' % self._limit)
|
||||
info.append(f'limit={self._limit}')
|
||||
if self._waiter:
|
||||
info.append('w=%r' % self._waiter)
|
||||
info.append(f'waiter={self._waiter!r}')
|
||||
if self._exception:
|
||||
info.append('e=%r' % self._exception)
|
||||
info.append(f'exception={self._exception!r}')
|
||||
if self._transport:
|
||||
info.append('t=%r' % self._transport)
|
||||
info.append(f'transport={self._transport!r}')
|
||||
if self._paused:
|
||||
info.append('paused')
|
||||
return '<%s>' % ' '.join(info)
|
||||
return '<{}>'.format(' '.join(info))
|
||||
|
||||
def exception(self):
|
||||
return self._exception
|
||||
@@ -431,8 +518,7 @@ class StreamReader:
|
||||
else:
|
||||
self._paused = True
|
||||
|
||||
@coroutine
|
||||
def _wait_for_data(self, func_name):
|
||||
async def _wait_for_data(self, func_name):
|
||||
"""Wait until feed_data() or feed_eof() is called.
|
||||
|
||||
If stream was paused, automatically resume it.
|
||||
@@ -442,8 +528,9 @@ class StreamReader:
|
||||
# would have an unexpected behaviour. It would not possible to know
|
||||
# which coroutine would get the next data.
|
||||
if self._waiter is not None:
|
||||
raise RuntimeError('%s() called while another coroutine is '
|
||||
'already waiting for incoming data' % func_name)
|
||||
raise RuntimeError(
|
||||
f'{func_name}() called while another coroutine is '
|
||||
f'already waiting for incoming data')
|
||||
|
||||
assert not self._eof, '_wait_for_data after EOF'
|
||||
|
||||
@@ -455,12 +542,11 @@ class StreamReader:
|
||||
|
||||
self._waiter = self._loop.create_future()
|
||||
try:
|
||||
yield from self._waiter
|
||||
await self._waiter
|
||||
finally:
|
||||
self._waiter = None
|
||||
|
||||
@coroutine
|
||||
def readline(self):
|
||||
async def readline(self):
|
||||
"""Read chunk of data from the stream until newline (b'\n') is found.
|
||||
|
||||
On success, return chunk that ends with newline. If only partial
|
||||
@@ -479,10 +565,10 @@ class StreamReader:
|
||||
sep = b'\n'
|
||||
seplen = len(sep)
|
||||
try:
|
||||
line = yield from self.readuntil(sep)
|
||||
except IncompleteReadError as e:
|
||||
line = await self.readuntil(sep)
|
||||
except exceptions.IncompleteReadError as e:
|
||||
return e.partial
|
||||
except LimitOverrunError as e:
|
||||
except exceptions.LimitOverrunError as e:
|
||||
if self._buffer.startswith(sep, e.consumed):
|
||||
del self._buffer[:e.consumed + seplen]
|
||||
else:
|
||||
@@ -491,8 +577,7 @@ class StreamReader:
|
||||
raise ValueError(e.args[0])
|
||||
return line
|
||||
|
||||
@coroutine
|
||||
def readuntil(self, separator=b'\n'):
|
||||
async def readuntil(self, separator=b'\n'):
|
||||
"""Read data from the stream until ``separator`` is found.
|
||||
|
||||
On success, the data and separator will be removed from the
|
||||
@@ -558,7 +643,7 @@ class StreamReader:
|
||||
# see upper comment for explanation.
|
||||
offset = buflen + 1 - seplen
|
||||
if offset > self._limit:
|
||||
raise LimitOverrunError(
|
||||
raise exceptions.LimitOverrunError(
|
||||
'Separator is not found, and chunk exceed the limit',
|
||||
offset)
|
||||
|
||||
@@ -569,13 +654,13 @@ class StreamReader:
|
||||
if self._eof:
|
||||
chunk = bytes(self._buffer)
|
||||
self._buffer.clear()
|
||||
raise IncompleteReadError(chunk, None)
|
||||
raise exceptions.IncompleteReadError(chunk, None)
|
||||
|
||||
# _wait_for_data() will resume reading if stream was paused.
|
||||
yield from self._wait_for_data('readuntil')
|
||||
await self._wait_for_data('readuntil')
|
||||
|
||||
if isep > self._limit:
|
||||
raise LimitOverrunError(
|
||||
raise exceptions.LimitOverrunError(
|
||||
'Separator is found, but chunk is longer than limit', isep)
|
||||
|
||||
chunk = self._buffer[:isep + seplen]
|
||||
@@ -583,20 +668,20 @@ class StreamReader:
|
||||
self._maybe_resume_transport()
|
||||
return bytes(chunk)
|
||||
|
||||
@coroutine
|
||||
def read(self, n=-1):
|
||||
async def read(self, n=-1):
|
||||
"""Read up to `n` bytes from the stream.
|
||||
|
||||
If n is not provided, or set to -1, read until EOF and return all read
|
||||
bytes. If the EOF was received and the internal buffer is empty, return
|
||||
an empty bytes object.
|
||||
If `n` is not provided or set to -1,
|
||||
read until EOF, then return all read bytes.
|
||||
If EOF was received and the internal buffer is empty,
|
||||
return an empty bytes object.
|
||||
|
||||
If n is zero, return empty bytes object immediately.
|
||||
If `n` is 0, return an empty bytes object immediately.
|
||||
|
||||
If n is positive, this function try to read `n` bytes, and may return
|
||||
less or equal bytes than requested, but at least one byte. If EOF was
|
||||
received before any byte is read, this function returns empty byte
|
||||
object.
|
||||
If `n` is positive, return at most `n` available bytes
|
||||
as soon as at least 1 byte is available in the internal buffer.
|
||||
If EOF is received before any byte is read, return an empty
|
||||
bytes object.
|
||||
|
||||
Returned value is not limited with limit, configured at stream
|
||||
creation.
|
||||
@@ -618,24 +703,23 @@ class StreamReader:
|
||||
# bytes. So just call self.read(self._limit) until EOF.
|
||||
blocks = []
|
||||
while True:
|
||||
block = yield from self.read(self._limit)
|
||||
block = await self.read(self._limit)
|
||||
if not block:
|
||||
break
|
||||
blocks.append(block)
|
||||
return b''.join(blocks)
|
||||
|
||||
if not self._buffer and not self._eof:
|
||||
yield from self._wait_for_data('read')
|
||||
await self._wait_for_data('read')
|
||||
|
||||
# This will work right even if buffer is less than n bytes
|
||||
data = bytes(self._buffer[:n])
|
||||
data = bytes(memoryview(self._buffer)[:n])
|
||||
del self._buffer[:n]
|
||||
|
||||
self._maybe_resume_transport()
|
||||
return data
|
||||
|
||||
@coroutine
|
||||
def readexactly(self, n):
|
||||
async def readexactly(self, n):
|
||||
"""Read exactly `n` bytes.
|
||||
|
||||
Raise an IncompleteReadError if EOF is reached before `n` bytes can be
|
||||
@@ -663,33 +747,24 @@ class StreamReader:
|
||||
if self._eof:
|
||||
incomplete = bytes(self._buffer)
|
||||
self._buffer.clear()
|
||||
raise IncompleteReadError(incomplete, n)
|
||||
raise exceptions.IncompleteReadError(incomplete, n)
|
||||
|
||||
yield from self._wait_for_data('readexactly')
|
||||
await self._wait_for_data('readexactly')
|
||||
|
||||
if len(self._buffer) == n:
|
||||
data = bytes(self._buffer)
|
||||
self._buffer.clear()
|
||||
else:
|
||||
data = bytes(self._buffer[:n])
|
||||
data = bytes(memoryview(self._buffer)[:n])
|
||||
del self._buffer[:n]
|
||||
self._maybe_resume_transport()
|
||||
return data
|
||||
|
||||
if compat.PY35:
|
||||
@coroutine
|
||||
def __aiter__(self):
|
||||
return self
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
@coroutine
|
||||
def __anext__(self):
|
||||
val = yield from self.readline()
|
||||
if val == b'':
|
||||
raise StopAsyncIteration
|
||||
return val
|
||||
|
||||
if compat.PY352:
|
||||
# In Python 3.5.2 and greater, __aiter__ should return
|
||||
# the asynchronous iterator directly.
|
||||
def __aiter__(self):
|
||||
return self
|
||||
async def __anext__(self):
|
||||
val = await self.readline()
|
||||
if val == b'':
|
||||
raise StopAsyncIteration
|
||||
return val
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user