Compare commits

..

938 Commits

Author SHA1 Message Date
Jiseok CHOI
d8f1d188c3 stdlib(sqlite): Raise ProgrammingError in closed Blob context manager (#6041) 2025-07-27 21:47:05 +09:00
Jeong, YunWon
89c58d678a Merge pull request #6037 from ShaharNaveh/update-support-init
Update `Lib/test/support/__init__.py` from 3.13.5
2025-07-27 10:50:52 +09:00
Jiseok CHOI
38ca076cb5 feat(stdlib/sqlite): Implement slice assignment for Blob (#6039) 2025-07-27 10:43:11 +09:00
ShaharNaveh
69f6423424 Try to have time_ns same as std 2025-07-26 20:01:21 +02:00
ShaharNaveh
d5793e04ec Don't import unittest.mock in test_int.py 2025-07-26 17:48:07 +02:00
ShaharNaveh
cbe975818e Remove extra spaces 2025-07-26 17:47:53 +02:00
ShaharNaveh
06196fa4f4 Merge remote-tracking branch 'upstream/main' into update-support-init 2025-07-26 17:31:26 +02:00
ShaharNaveh
0d1a02583a Have time_ns for wasi 2025-07-26 17:30:46 +02:00
Jack O'Connor
4079776c36 Add kde function and tests to RustPython statistics module (#6030)
* Copy CPython 3.13 statistics module into RustPython

* Adjust CPython "magic constants" in KDE tests

## test_kde

I'm not too sure why but this one takes a few seconds to run the second
for loop which calculates the cumulative distribution and does a rough
calculation of the area under the curve.

## test_kde_random

I have a lower bound for RustPython to sort a random list of 1_000_000
numbers on my laptop of > 1 hour. By dropping n to 30_000 sort will not
take an egregious amount of time to run. It is then necessary to lower
the tolerance for the math.isclose check, or the computed values may
**randomly** fail due to the higher variance caused by the smaller
sample size.

* Reintroduce expected failure in test_statistics.TestNormalDict.test_slots

* Sync Rust `normal_dist_inv_cdf` with Python equivalent

See https://github.com/python/cpython/pull/95265.

To quote:
> Restores alignment with random.gauss(mu, sigma) and
random.normalvariate(mu, sigma) both. of which are equivalent to
sampling from NormalDist(mu, sigma).inv_cdf(random()). The two functions
in the random module happy accept sigma=0 and give a well-defined
result.

> This also lets the function gently handle a sigma getting smaller,
eventually becoming zero. As sigma decrease, NormalDist(mu,
sigma).inv_cdf(p) forms a tighter and tighter internal around mu and
becoming exactly mu in the limit. For example, NormalDist(100,
1E-300).inv_cdf(0.3) cleanly evaluates to 100.0but withsigma=1e-500``
the function previously would raised an unexpected error.
2025-07-26 00:42:22 +09:00
ShaharNaveh
b829333f1d Fix patch of gc_threshold 2025-07-25 16:59:37 +02:00
ShaharNaveh
0e3ff8ae5f Fix patch of disable_gc 2025-07-25 16:58:25 +02:00
ShaharNaveh
73f5ceb79b Update test_int.py 2025-07-25 16:20:36 +02:00
ShaharNaveh
a5b240aab8 skip crashing test 2025-07-25 16:13:07 +02:00
ShaharNaveh
0648e975d9 Upsate test_dictviews 2025-07-25 16:07:59 +02:00
ShaharNaveh
5d9f9acb1d Update test_collections.py 2025-07-25 16:06:00 +02:00
ShaharNaveh
26cdbfe048 Mark tests correctly 2025-07-25 15:59:14 +02:00
ShaharNaveh
17e60754f6 Fix patching of support 2025-07-25 15:47:48 +02:00
ShaharNaveh
bb08398957 Update test_userlist.py 2025-07-25 15:47:32 +02:00
ShaharNaveh
0d1a68dfab mark failing tests in test_picklebuffer.py 2025-07-25 15:41:37 +02:00
ShaharNaveh
7f97034055 Mark failing tests 2025-07-25 14:53:03 +02:00
ShaharNaveh
409f5dda9f Add more code to support/__init__.py 2025-07-25 14:53:03 +02:00
ShaharNaveh
68cd33f37e Update pickle tests from 3.13.5 2025-07-25 14:53:03 +02:00
ShaharNaveh
4fb5736694 Add run_doctest 2025-07-25 14:53:03 +02:00
ShaharNaveh
b51f6de0c8 Reapply RustPython patches 2025-07-25 14:53:03 +02:00
ShaharNaveh
3058d99fd5 Copy parts of old code to make libregtest work 2025-07-25 14:53:03 +02:00
ShaharNaveh
74201365c6 Update Lib/test/support/__init__.py from 3.13.5 2025-07-25 14:53:03 +02:00
Shahar Naveh
c232b7f1f8 Update csv.py from 3.13.5 (#6035)
* Use `raise_if_stop!` macro

* Update `csv.py` from 3.13.5

* Mark failing tests
2025-07-25 19:08:01 +09:00
Shahar Naveh
ae03bacb39 Update {_py,}decimal.py from 3.13.5 (#6034) 2025-07-25 19:04:49 +09:00
Jiseok CHOI
fb9147736d stdlib(sqlite3): Raise ProgrammingError for missing named parameter (#6036) 2025-07-25 19:02:56 +09:00
Shahar Naveh
9499d39f55 Update html from 3.13.5 (#6031) 2025-07-25 10:58:21 +09:00
Shahar Naveh
6a9579efc7 Update trace.py from 3.13.5 (#6029) 2025-07-25 10:50:41 +09:00
Shahar Naveh
8621b3d7da Added pyclbr from 3.13.5 (#6028) 2025-07-25 10:50:17 +09:00
Shahar Naveh
24f2524e6e Added "os.wait" tests from 3.13.5 (#6027)
* Added "wait" tests from 3.13.5

* Trigger CI
2025-07-25 10:49:05 +09:00
Shahar Naveh
74bee7cbbe Update codeop.py from 3.13.5 (#6026) 2025-07-25 10:47:20 +09:00
Shahar Naveh
01edb93957 Resolve libc::RLIM_NLIMITS warning on android (#6025)
* Resolve warning on android

* Apply CodeRabbit suggestion
2025-07-25 10:46:32 +09:00
Jiseok CHOI
bcf56279ec Raise UnicodeEncodeError for surrogates in sqlite.executescript (#6024) 2025-07-25 10:45:33 +09:00
Shahar Naveh
6bce5e1616 Cleanup vm/src/stdlib/stat.rs (#6018) 2025-07-25 10:44:49 +09:00
Shahar Naveh
b7336366cb Update test_enumerate.py from 3.13.5 (#6032) 2025-07-25 10:43:16 +09:00
Shahar Naveh
96f47a415e Export ruff_source_file types in rustpython_compiler_core (#6020)
* export ruff types in `compiler::core`

* Use exported ruff types in `vm/**`

* unlink `ruff_source_file` as a direct dependency
2025-07-23 12:28:38 +09:00
Shahar Naveh
582e25b11b Update tabnanny.py from 3.13.5 (#6021) 2025-07-23 12:27:56 +09:00
Shahar Naveh
d897f9e0e0 Unpin Rust nightly in CI (#6022) 2025-07-23 12:27:41 +09:00
Shahar Naveh
9995cc60b5 Update warnings from 3.13.5 (#6019) 2025-07-23 12:27:26 +09:00
Shahar Naveh
ba22ad2c0c Replace compiler::source module with ruff_source_file (#6016)
* Replace `compiler::source`` with ruff

* Require `ruff_source_file`
2025-07-22 20:52:25 +09:00
Jiseok CHOI
57bdf35ee6 Enforce valid UTF-8 encoding for sqlite collation names (#6015)
* Make public `PyStr::ensure_valid_utf8`

* Enforce valid UTF-8 encoding for sqlite collation names
2025-07-21 23:42:47 +09:00
Jiseok CHOI
bbe98ddd86 Construct detailed message on text decode failure (#6014) 2025-07-21 22:33:09 +09:00
Jeong, YunWon
52395497dd Update test_typing from Python 3.13.5 (#6013) 2025-07-21 21:14:25 +09:00
Jeong, YunWon
bd7947ec8f Fix test_typing.test_args_kwargs (#6012) 2025-07-21 16:54:27 +09:00
Jeong, YunWon
a1ee7f5461 Fix __dict__ getset type (#6010) 2025-07-21 16:13:17 +09:00
Jeong, YunWon
cd58d154cf Type alias type (#6011)
* Constructor for TypeAliasType

* Fix PyBaseObject::py_new

* Representable for TypeAliasType
2025-07-21 14:42:18 +09:00
Jeong, YunWon
3bce41baab Fix test_typing.test_unpack_wrong_type (#6009) 2025-07-21 00:54:14 +09:00
Jeong, YunWon
99c1afe0be compile_subscript (#6008)
* ListToTuple

* star_unpack_helper
2025-07-21 00:53:36 +09:00
Shahar Naveh
c497061290 Update json from 3.13.5 (#6007)
* Update `json` from 3.13.5

* Update `test_json` from 3.13.5
2025-07-20 18:44:46 +09:00
Shahar Naveh
8177525d49 Update ast.py from 3.13.5 (#6006) 2025-07-20 18:37:38 +09:00
Jeong, YunWon
4e0c1aa83d compile_type_param_bound_or_default (#6005) 2025-07-20 17:12:56 +09:00
Jiseok CHOI
ff35dcd95a feat(vm/slot): implement Py_TPFLAGS_MANAGED_DICT for class objects (#5949) 2025-07-20 14:11:36 +09:00
Jeong YunWon
5284b73320 Refactor compile_function 2025-07-20 10:57:24 +09:00
Jeong YunWon
7736df030a Fix scope error 2025-07-20 10:57:24 +09:00
Jeong YunWon
6b773f6e14 Fix is_optimized 2025-07-20 10:57:24 +09:00
Jiseok CHOI
6f80ac0edd Align SQL comment parsing with CPython (#5996) 2025-07-20 10:51:42 +09:00
Shahar Naveh
d7a9b69995 Update stat from 3.13.5 (#5992)
* Update stat from 3.13.5

* Set correct value for S_IFWHT on macos
2025-07-20 10:37:43 +09:00
Shahar Naveh
4f9dd41041 Update _weakrefset.py from 3.13.5 (#6004)
* Update `_weakrefset` from 3.13.5

* Update `test_weakset.py` from 3.13.5
2025-07-20 10:33:22 +09:00
Shahar Naveh
9739592798 Update logging from 3.13.5 (#6003)
* Update logging from 3.13.5

* Mark failing tests

* Mark some failing windows tests

* Skip flaky test

* Mark more failing tests
2025-07-20 10:33:02 +09:00
Shahar Naveh
cb6057a50c Update selector.py from 3.13.5 (#6002) 2025-07-20 10:31:58 +09:00
Shahar Naveh
11b8e73566 Merge pull request #6000 from ShaharNaveh/update-mp
Update multiprocess from 3.13.5
2025-07-20 10:31:30 +09:00
Jeong, YunWon
da5a44ee01 Pin rustc nightly to nightly-2025-07-18 for miri tests (#5999)
* Pin rustc nightly to `nightly-2025-07-18` for miri tests

* Fix indent

* Set the miri rustc channel via env var
2025-07-20 00:33:49 +09:00
ShaharNaveh
95880cee72 Set the miri rustc channel via env var 2025-07-19 11:42:49 +02:00
ShaharNaveh
6b1e4a7964 Fix indent 2025-07-19 11:30:43 +02:00
Shahar Naveh
fa7849d43f Pin rustc nightly to nightly-2025-07-18 for miri tests 2025-07-19 11:28:45 +02:00
Jiseok CHOI
bd8e557b70 test(importlib): Enable bad bytecode tests for PEP451 loaders (#5997) 2025-07-19 12:16:19 +09:00
Jiseok CHOI
f8d03fd680 Correctly implement isolation_level setter to handle deletion (#5983) 2025-07-18 23:08:51 +09:00
Jeong, YunWon
799f38baea Merge pull request #5978 from ShaharNaveh/update-test-exception
Update `test_exception*.py` from 3.13.5
2025-07-17 13:55:25 +09:00
Shahar Naveh
1fcb656363 Set {min,max}_arity (#5994)
* General cleanup

* Compute {min,max}_arity
2025-07-17 13:49:25 +09:00
Shahar Naveh
80a9e0ed54 Update rlcompleter from 3.13.5 (#5990) 2025-07-17 13:44:18 +09:00
Shahar Naveh
559a7a56e5 Update _colorize.py from 3.13.5 (#5988) 2025-07-17 13:43:32 +09:00
Jiseok CHOI
5f1290d86e Implement properties on _TextIOBase and StringIO (#5987) 2025-07-17 13:42:38 +09:00
Shahar Naveh
63e6c01924 CI: Increase windows timeout from 40 -> 45 (#5993) 2025-07-17 00:04:51 +09:00
Shahar Naveh
04407be6b2 Update secrets from 3.13.5 (#5991) 2025-07-17 00:04:12 +09:00
Shahar Naveh
a0a6f735a1 Update quopri from 3.13.5 (#5989) 2025-07-17 00:03:25 +09:00
ShaharNaveh
4515c614bf mark more tests 2025-07-16 13:37:18 +03:00
ShaharNaveh
9e22580a95 Update test_faulthandler from 3.13.5 2025-07-16 13:37:18 +03:00
ShaharNaveh
058c76cee8 mark a failing test 2025-07-16 13:37:18 +03:00
ShaharNaveh
323b2da2dd Remove duplicated class name 2025-07-16 13:37:18 +03:00
ShaharNaveh
55e2c97154 Don't require debug_ranges 2025-07-16 13:37:18 +03:00
ShaharNaveh
2c87988f8d Update test_traceback.py from 3.13.5 2025-07-16 13:37:18 +03:00
ShaharNaveh
f6d755b4ff mark failing tests 2025-07-16 13:37:18 +03:00
ShaharNaveh
0fbe6ce268 Update test_exceptions from 3.13.5 2025-07-16 13:37:18 +03:00
ShaharNaveh
e064f8cef2 Update test_exception_variations.py from 3.13.5 2025-07-16 13:37:18 +03:00
ShaharNaveh
f53a8d919a Don't skip passing test 2025-07-16 13:37:18 +03:00
ShaharNaveh
966d6d2d26 Update test_exception_group.py from 3.13.5 2025-07-16 13:37:18 +03:00
Jeong, YunWon
6a3dff63bb Downcastable (#5986)
* simplify int power

* downcastasble

* deprecate payload*
2025-07-16 18:00:18 +09:00
Jiseok CHOI
177bfb7077 Remove deleter attribute from pygetset (#5984) 2025-07-16 09:44:14 +09:00
Shahar Naveh
5309e8c7c4 Fix invalid args count msg (#5960) 2025-07-16 01:54:51 +09:00
Jiseok CHOI
5957f5d31a Fix off-by-one error in SQL length limit check (#5982) 2025-07-16 01:53:20 +09:00
Jiseok CHOI
f465af3a7c Reject SQL queries containing null characters (#5981) 2025-07-16 01:52:28 +09:00
Jiseok CHOI
6d2152cafe Correctly handle None for protocol in adapt(..) (#5979) 2025-07-16 01:49:53 +09:00
Shahar Naveh
a54873d302 Update shlex from 3.13.5 (#5977) 2025-07-15 21:27:05 +09:00
Jeong, YunWon
b965ce7392 Remove misplaced SymbolScope::TypeParams (#5975)
* Rename SymbolTableType -> CompileScope

* Remove SymbolScope::TypeParams
2025-07-15 17:56:19 +09:00
Jeong, YunWon
2dd0ce54f9 remove ellipsis() (#5973) 2025-07-15 16:10:40 +09:00
Jeong, YunWon
1d3603419e SetFunctionAttribute (#5968)
* PyRef::into_non_null

* SetFunctionAttribute

* set_function_attribute

* frame helper in PyFuncion

* remove closure lock

* cleanup unused args
2025-07-15 03:12:23 +09:00
Jiseok CHOI
d4f85cf073 Provide detailed error for circular from imports (#5972) 2025-07-15 01:45:42 +09:00
Jiseok CHOI
ed433837b3 Introduce PyUtf8Str and fix(sqlite): validate surrogates in SQL statements (#5969)
* fix(sqlite): validate surrogates in SQL statements

* Add `PyUtf8Str` wrapper for safe conversion
2025-07-15 00:54:42 +09:00
Jeong, YunWon
fd35c7a706 Impl Drop for PyAtomicRef (#5970) 2025-07-14 22:54:44 +09:00
Jeong YunWon
dd4f0c3a9f fix lint 2025-07-14 20:40:54 +09:00
Jeong YunWon
406be9cd15 Upgrade radium to 1.1.1 2025-07-14 20:40:54 +09:00
Jeong, YunWon
eef8890f32 Integrate PyTupleTyped into PyTuple (#5959)
* Fix derive(Traverse)

* PyPayload::payload_type_of

* PyTupleTyped as alias of PyTuple

* Fully integrate PyTupleTyped into PyTuple
2025-07-14 18:43:18 +09:00
Jeong YunWon
6342ad4fa7 Fully integrate PyTupleTyped into PyTuple 2025-07-14 14:27:48 +09:00
Jeong YunWon
14ce76e6c8 PyTupleTyped as alias of PyTuple 2025-07-14 14:27:48 +09:00
Jeong YunWon
09489712e6 PyPayload::payload_type_of 2025-07-14 14:27:48 +09:00
Jeong YunWon
635b4afff1 Fix derive(Traverse) 2025-07-14 14:27:48 +09:00
Shahar Naveh
36f4d30e01 Update test_tuple.py from 3.13.5 (#5966) 2025-07-14 14:26:08 +09:00
Shahar Naveh
4fe4ff4f99 Update test_{list,listcomps}.py from 3.13.5 (#5965) 2025-07-14 14:24:00 +09:00
Jiseok CHOI
5ab64b7002 fix(sqlite): align adaptation protocol with CPython (#5964) 2025-07-14 14:22:52 +09:00
Shahar Naveh
97e85b220e Update test_{dict,weakref}.py from 3.13.5 (#5963)
* Update test_dict.py from 3.13.5

* Update `test_weakref.py` from 3.13.5
2025-07-14 14:21:59 +09:00
Jiseok CHOI
d42e8f0042 fix(sqlite): produce correct error for surrogate characters (#5962) 2025-07-14 14:21:36 +09:00
Shahar Naveh
ed8d7157d9 Update test_{complex,float}.py from 3.13.5 (#5961) 2025-07-14 14:20:28 +09:00
Ashwin Naren
04d8d69a8c upgrade parts of test.support (#5686) 2025-07-14 14:19:33 +09:00
Jeong, YunWon
8ab7aa2c6b type.__dict__ (#5957) 2025-07-13 13:12:03 +09:00
Jeong, YunWon
16aaad7aeb PyTraceback Constructor (#5958) 2025-07-13 10:20:07 +09:00
Jeong, YunWon
52d46326de make_closure (#5955) 2025-07-13 01:00:15 +09:00
Jeong, YunWon
e21ec550d4 Fix set___name__ and set___qualname__ deadlock (#5956) 2025-07-13 00:22:41 +09:00
Shahar Naveh
ac20b00e26 str.replace support count as keyword arg (#5954) 2025-07-12 22:43:47 +09:00
Shahar Naveh
e75aebb967 Update str related tests from 3.13.5 (#5953)
* Update str related tests from 3.13.5

* Apply RustPython patches

* Mark new failing tests
2025-07-12 20:44:34 +09:00
Jeong, YunWon
fef660e6b3 more PEP695 (#5917)
* compile_class_body

* type.__orig_bases__ regression of test_all_exported_names

* rework type_params scope

* refactor compile_class_def
2025-07-12 20:42:57 +09:00
Jeong, YunWon
3ef0cfc50c compiler enter_scope (#5950)
* enter_scope

* drop_class_free

* push_output based on enter_scope
2025-07-12 19:28:22 +09:00
Shahar Naveh
1303ace453 Update textwrap from 3.13.5 (#5952) 2025-07-12 19:18:31 +09:00
Shahar Naveh
3f9a5fddbb Don't skip non hanging test (#5951) 2025-07-12 19:18:08 +09:00
Shahar Naveh
f19478edec Update operator from 3.13.5 (#5935) 2025-07-12 01:10:30 +09:00
Jeong, YunWon
c4234c1692 SymbolTable::varnames, fblock (#5948)
* SymbolTable::varnames

* varname_cache copies it

* fasthidden & static attributes

* metadata

* fblock
2025-07-11 22:43:08 +09:00
Shahar Naveh
c3967bf849 Set timeout for CI (#5947) 2025-07-11 19:18:23 +09:00
Jeong, YunWon
59c7fcbb98 compiler set_qualname (#5930)
* set_qualname

* remove qualified_path
2025-07-11 18:21:51 +09:00
Shahar Naveh
50c241fd71 Fix yaml error in take issue command (#5946) 2025-07-11 17:44:46 +09:00
Jeong, YunWon
392f9c26c5 Instruction::Resume (#5944)
* ImportStar

* Instruction::Resume
2025-07-11 17:25:57 +09:00
Jeong, YunWon
0ae6b4575c typing TypeAlias (#5945) 2025-07-11 16:16:01 +09:00
Jeong, YunWon
8b6c78c884 SymbolTableType::Lambda (#5942) 2025-07-11 13:35:52 +09:00
Jeong, YunWon
9b133b8560 CodeInfo::private (#5943) 2025-07-11 13:11:15 +09:00
Jeong, YunWon
2f94a63958 Add SymbolUsage::TypeParams (#5941) 2025-07-11 11:24:20 +09:00
Shahar Naveh
2c30e01ae2 Update test_deque from 3.13.5 (#5939) 2025-07-11 08:36:34 +09:00
Shahar Naveh
01f15065fa Use raise_if_stop! macro where possible (#5938) 2025-07-11 08:36:08 +09:00
Shahar Naveh
38837e587b Make take issue comment to use curl (#5937)
* Revert "Add missing `@` for the "take" comment command (#5933)"

This reverts commit ef385a9efa.

* Fix `take`
2025-07-11 08:35:21 +09:00
Shahar Naveh
089c39f741 Update test_string_literals.py from 3.13.5 (#5934) 2025-07-10 22:47:55 +09:00
Jiseok CHOI
4c7523080a fix(format): isolate special grouping rule to sign-aware zero-padding (#5924) 2025-07-10 22:47:24 +09:00
Shahar Naveh
ef385a9efa Add missing @ for the "take" comment command (#5933) 2025-07-10 22:46:29 +09:00
Shahar Naveh
b2013cddc9 Add "take" comment command (#5932) 2025-07-10 19:55:38 +09:00
Jiseok CHOI
8c4c63673e fix(itertools): add re-entrancy guard to tee object (#5931)
* fix(itertools): add re-entrancy guard to tee object

* apply feedback PyRwLock -> PyMutex & remove AtomicCell<bool> lock field
2025-07-10 18:11:24 +09:00
Jeong, YunWon
18d7c1baf1 codeobj.qualname (#5929) 2025-07-10 10:27:03 +09:00
yt2b
f608df4a23 Formatting with width and separator doesn't work correctly (#5927)
* Fix add_magnitude_separators

* Add extra tests
2025-07-10 09:10:52 +09:00
Jeong, YunWon
54ff198409 Upgrade Lib/types.py from Python 3.13.5 (#5928) 2025-07-10 08:57:08 +09:00
Shahar Naveh
cbd9b30bd1 Update compileall from 3.13.5 (#5914) 2025-07-10 08:36:37 +09:00
Shahar Naveh
5985ec8be0 sys.platform android & ios (#5921)
* Make sys.platform to return 'android'

* Added missing 'ios' platform
2025-07-10 00:47:24 +09:00
Jiseok CHOI
3a6a766a03 fix(sqlite): implement PARSE_COLNAMES column name parsing (#5923) 2025-07-10 00:18:40 +09:00
Jiseok CHOI
e6fdef43dc fix(sqlite): autocommit mode transaction handling to match CPython (#5918) 2025-07-09 20:13:26 +09:00
Shahar Naveh
f0cf9e6492 Update cmd from 3.13.5 (#5920)
* Update cmd from 3.13.5

* Add `test.support.pty_helepr` from 3.13.5
2025-07-09 19:03:53 +09:00
Jiseok CHOI
2f9459cf02 Enable match statements sqlite cli (#5919) 2025-07-09 19:03:17 +09:00
Jiseok CHOI
341341520e Fix SQLite large integer overflow error handling (#5916) 2025-07-09 17:14:45 +09:00
Shahar Naveh
c195473a29 Update copy from 3.13.5 (#5913) 2025-07-08 23:49:57 +09:00
Shahar Naveh
d58c500129 Update gzip from 3.13.5 (#5912) 2025-07-08 20:13:52 +09:00
Shahar Naveh
5c8b027af4 Update test_datetime from 3.13.5 (#5911) 2025-07-08 19:46:26 +09:00
Jeong, YunWon
ec577e556d Merge pull request #5909 from youknowone/typing-parameters
typing __parameters__ __type_params__
2025-07-08 00:08:45 +09:00
Jeong YunWon
bd54e537fd Fix __parameters__, __type_params__ 2025-07-07 23:23:24 +09:00
Jeong YunWon
481e03abe4 Replace old anndata to new from CPython 3.13.5 2025-07-07 21:18:48 +09:00
Jeong YunWon
999976a76c fix __parametesr__ 2025-07-07 19:05:04 +09:00
Jeong YunWon
7ebe0182e4 More eval typecheck 2025-07-07 19:02:37 +09:00
Jeong YunWon
a576569a02 update instructions 2025-07-07 19:02:37 +09:00
Jiseok CHOI
240f87a911 Handle negative time.sleep values (#5906)
* fix(time): Handle negative sleep values
2025-07-07 13:49:12 +09:00
yt2b
23a5c82a3a Add separator validation (#5904)
* Add separator validation

* Remove @unittest.expectedFailure
2025-07-06 23:08:07 +09:00
Shahar Naveh
9336507be6 Fix clippy::inconsistent_struct_constructor (#5905) 2025-07-06 09:24:05 +09:00
Shahar Naveh
3c7ec04285 Update tomllib from 3.13.5 (#5902) 2025-07-04 23:47:34 +09:00
Shahar Naveh
f57f6d7443 Make gemini ignore Lib/** (#5903) 2025-07-04 23:47:05 +09:00
Shahar Naveh
94a55eb479 Update uuid from 3.13.5 (#5901)
* Update uuid from 3.13.5

* Mark failing test
2025-07-04 22:37:12 +09:00
yt2b
0a59c1cad3 Implement PyComplex's __format__ function (#5900)
* Add num-complex in rustpython-common

* Implement PyComplex's __format__ function

* Remove @unittest.expectedFailure

* Fix PyComplex's __format__ function

* Remove @unittest.expectedFailure

* Add extra tests

* Rename to ZeroPadding and AlignmentFlag
2025-07-04 22:35:28 +09:00
Shahar Naveh
694fe50241 Use const fn where possible (#5894) 2025-07-04 22:26:20 +09:00
coderabbitai[bot]
5b20bb851e Remove conflicting AsRef<Self> implementation for PyObject in core.rs (#5899)
* 📝 CodeRabbit Chat: Remove conflicting AsRef<Self> implementation for PyObject in core.rs

* Update vm/src/object/core.rs

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-07-04 11:40:30 +09:00
Shahar Naveh
4bd328906e Update resource from 3.13.5 (#5893) 2025-07-04 10:09:06 +09:00
Shahar Naveh
69545c0798 Use Self where possible (#5892) 2025-07-04 10:08:43 +09:00
Shahar Naveh
05013e3d0b macro for pyast code depulication (#5887) 2025-07-04 08:45:44 +09:00
Jeong, YunWon
8a2a6af91b __type_params__ in __build_class__ (#5883)
* remove future __classs_getitem__

* __type_params__ in __build_class__
2025-07-03 14:08:42 +09:00
Jeong, YunWon
c529c247bb PyRef::into_ref() (#3744) 2025-07-03 14:07:30 +09:00
Jeong, YunWon
49422c5819 relocate Generic under typevar.rs (#5889) 2025-07-03 12:05:03 +09:00
Jeong, YunWon
d40cbbb451 Fix cspell warning (#5890)
* Fix cspell warning

* cspell: -> spell-checker:
2025-07-03 12:04:43 +09:00
Jeong, YunWon
fffa735868 __class_getitem__ (#5888) 2025-07-03 11:01:53 +09:00
Shahar Naveh
b6acfe0f9d Add raise_if_stop! macro for protocol/iter.rs (#5885) 2025-07-03 08:37:54 +09:00
Shahar Naveh
43be4e48ad nitpicks (#5886)
* Use `Self` instead of struct name

* General fixes

* General

* General fixes

* More nitpicks

* Added newlines

* More nitpicks

* Use Self where possible

* Use Self where possible

* Use Self

* Use Self more

* Use Self
2025-07-02 21:27:47 +09:00
Shahar Naveh
18fdf85510 Fix itertools.pairwise (#5884)
* Fix pairwise hangs

* Don't skip failing tests;)

* Drop lock of read, not write
2025-07-02 21:10:24 +09:00
Jeong YunWon
fa7af0e5ea type.__type_params__ 2025-07-01 19:05:29 +09:00
Jeong YunWon
e25c2856ad cspell ignore target/** 2025-07-01 19:05:29 +09:00
Jeong, YunWon
3d951a883a Compile starred annotations (#5881)
* Fix starred annotation

* uncomment starred annotation
2025-07-01 17:20:42 +09:00
Jeong, YunWon
4ebd485694 typing _Py_subs_paramters (#5880) 2025-07-01 16:09:59 +09:00
Shahar Naveh
9f3c34a705 Update ipaddress (#5878) 2025-07-01 10:59:41 +09:00
Jeong, YunWon
119e209ffd fix coderabbit (#5877) 2025-07-01 04:40:58 +09:00
Jeong, YunWon
334a5a7c3c Iterable for PyGenericAlias (#5876)
* Iterable for PyGenericAlias

* GenericAlias works

* typevar.rs
2025-07-01 04:35:28 +09:00
Jeong, YunWon
e7c18f19ab Merge pull request #5850 from youknowone/typing
Upgrade typing to 3.13.5
2025-07-01 02:31:58 +09:00
Jeong YunWon
3f5566da53 Fix _typing.override 2025-07-01 00:10:16 +09:00
Jeong YunWon
c0b9694cda Patch typing.py to let _py_abc fallback 2025-07-01 00:10:15 +09:00
Jeong YunWon
5a2007fd46 renames 2025-07-01 00:10:15 +09:00
Jeong YunWon
39d091f01c Fix typing 2025-07-01 00:10:15 +09:00
Jeong YunWon
d5946d496d Update typing.py from CPython 3.13.5 2025-07-01 00:10:15 +09:00
Jeong, YunWon
c7641260dd Make coderabbit ignore CPython files (#5875) 2025-07-01 00:02:11 +09:00
Shahar Naveh
01117f4a30 Update argparse to 3.13.5 (#5874)
* Update argparse from 3.13.5

* Mark failing test
2025-06-30 23:56:34 +09:00
Jeong, YunWon
d5d3507921 Add sys._getframemodulename (#5873) 2025-06-30 22:50:27 +09:00
Shahar Naveh
3bde2ddac3 Update base64 3.13.5 (#5872) 2025-06-30 19:09:05 +09:00
Lee Dogeon
5953a938bd chore: test testcases with @unittest.skip decorator (#5871)
Signed-off-by: Lee Dogeon <dev.moreal@gmail.com>
2025-06-30 19:07:58 +09:00
Jeong, YunWon
b59a6666fe __repr__ in typing (#5870)
* ParamSpec.__repr__

* Fix more repr
2025-06-30 10:36:10 +09:00
Jeong, YunWon
66a1138c66 Fix isinstance (#5869)
* lookup_special

* Fix abstract_issubclass

* Fix real_is_instance
2025-06-30 10:35:52 +09:00
Shahar Naveh
b6ff541260 Create ContentItem with a macro (#5859)
* Create ContentItem with a macro

* Set struct docs via metadata
2025-06-30 00:22:52 +09:00
Jeong, YunWon
7c6d0632cc Update isintance (#5868)
* PyUnion::get_args

* issubclass

* isinstance
2025-06-29 15:15:09 +09:00
Jeong, YunWon
4cca8b0d32 Callable __or__ patch (#5753)
* callable __or__ patch

* add test
2025-06-29 14:43:58 +09:00
Ashwin Naren
159769876f add test 2025-06-29 14:42:09 +09:00
Jeong, YunWon
47a7a00fbf Rework issubclass (#5867)
* check_exact

* check_class

* Type compatibility tools

* abstract_issubclass

* recursive_issubclass
2025-06-29 14:28:29 +09:00
Ashwin Naren
0a1c8c3c7e callable __or__ patch 2025-06-29 14:04:00 +09:00
Jeong, YunWon
7a6e5c465f Upgrade abc.py _py_abc.py to 3.13.5 (#5865) 2025-06-29 12:49:14 +09:00
Jeong, YunWon
28dff8af6c Fix TypeParams, TypeAlias compile (#5862) 2025-06-29 11:24:15 +09:00
Jeong, YunWon
0ef22ab6f2 Fix type.__subclasscheck__ (#5864) 2025-06-29 11:21:07 +09:00
Jeong, YunWon
4ebecc0f5e Remove PyTuple::fast_getitem (#5863) 2025-06-29 05:31:21 +09:00
Jeong, YunWon
07a04acfaa Allow heap getset creation (#5854)
* Update _threading_local.py

* allow heap-getset
2025-06-29 03:03:19 +09:00
Ashwin Naren
3a85499b11 Display logo on windows executable (#5790) 2025-06-29 02:50:05 +09:00
Lee Dogeon
8589b55203 Avoid set changed size during iteration (#5860) 2025-06-29 02:14:22 +09:00
Shahar Naveh
8cac4335b4 Constify functions & General nitpicks (#5858)
* General cleanups

* Add some newlines

* Use bitlag match

* More constify

* Don't convert to str twice

* constify more

* Use match directly instead of if & match!

* Constify more

* Constify more

* more consts

* Constify more

* Constify more

* Don't use bitflags_match macro
2025-06-28 23:08:30 +09:00
Wildan M
e41d7f523a Fix stat module compilation for Redox OS 2025-06-28 19:38:26 +09:00
Jeong, YunWon
2bd52121bb Align is_instance (#5855)
* Align is_instance

* Upgrade test_isinstance to 3.13.5

* patch test_isinstance
2025-06-28 11:59:48 +09:00
Lee Dogeon
910077d7ae Implement _stat module (#5847)
* Implement _stat module

* Allow 'FIRMLINK' term
2025-06-28 03:15:46 +09:00
Jeong, YunWon
ac26be7fd7 Fix getset.__objclass__ (#5853) 2025-06-28 00:35:05 +09:00
Jeong, YunWon
b9b1c8580f slot_repr for structseq (#5852) 2025-06-27 23:14:24 +09:00
Jeong, YunWon
43302cec4d Fix boundmethod not to have __dict__ + immutable type (#5851) 2025-06-27 22:50:58 +09:00
Jeong, YunWon
f5ccd4faed Fix __annotation__ (#5849) 2025-06-27 20:36:19 +09:00
Jeong, YunWon
5504f6d9e8 heaptype __qualname__ (#5848) 2025-06-27 19:58:48 +09:00
Jeong, YunWon
f0c7cb26f7 fn class #[inline] (#5846) 2025-06-27 16:02:44 +09:00
Jeong, YunWon
0095941fb7 ruff format (#5845) 2025-06-27 15:06:35 +09:00
Jeong, YunWon
6b6534508f Fix type_params lifetime in symboltable (#5844)
* fix cspell

* Fix type_params lifetime in symboltable
2025-06-27 14:32:54 +09:00
Narawit Rakket
2ea77b4442 Implement true hash for tuple (#3460) 2025-06-27 14:27:27 +09:00
Ashwin Naren
5a81533f61 stdlib compatability checking scripts (#5697)
* stdlib compat checking scripts

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>

* update output

---------

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-06-27 13:49:53 +09:00
Jeong, YunWon
a18029ee89 [BREAKING CHANGE] Merge pull request #5842 from youknowone/magic-method-names
[BREAKING CHANGE] Writing magic method names as full name
2025-06-27 13:36:55 +09:00
Jeong YunWon
3673372d3d Fix cspell warnings 2025-06-27 12:32:59 +09:00
Jeong YunWon
993ea8923c Rename members 2025-06-27 12:32:59 +09:00
Jeong YunWon
ef3cf70e30 Remove magic from macro 2025-06-27 12:32:59 +09:00
Jeong YunWon
4378a61c8b Fix 1.80 build 2025-06-27 12:32:59 +09:00
Jeong, YunWon
fe2c9bf361 Fix stable clippy (#5843) 2025-06-27 12:12:21 +09:00
Jeong, YunWon
0d492a6b02 Fix genericalias parameters (#5841)
* genericalias

* Fix attr_exceptions list

* impl TypeVarTuple iter

* Update _collections_abc.py to Python 3.13

* genericalias.__unpack__
2025-06-27 10:32:26 +09:00
Jeong, YunWon
3031d5ba45 more typing (#5840)
* rename typing

* No test code modification rules

* remove orphan TODOs

* mro_entires

* typing module name

* typing_subst

* typing.override

* Comparable for typing

* remove to_owned() from typing
2025-06-26 21:51:30 +09:00
Jeong, YunWon
6905d4375b typing ParamSpec (#5837)
* Add cspell

* TypeVar

* ParamSpec

* refactor

* TypeVarTuple repr

* Remove expectedFailure
2025-06-26 20:31:17 +09:00
Shahar Naveh
1ae07813ee Remove redundent to_owned() and to_string() calls (#5836)
* Remove redundent String conversion for errors

* cargo fmt
2025-06-25 23:42:26 +09:00
Jeong, YunWon
f9d9307503 typing.TypeVar (#5834)
* copilot instruction

* typing.TypeVar

* typing.NoDefault
2025-06-25 08:37:40 +09:00
Shahar Naveh
ab09de8542 Remove unnecessary string conversions in error message construction (#5826)
* Make `new_X_error` recv `impl Into<String>`

* Maks the rest of the methods to be generic

* Remove `.to_owned()` from more files
2025-06-25 01:43:41 +09:00
Jeong, YunWon
6a992d4fa2 Add hashlib hasher repr (#5833) 2025-06-24 20:09:24 +09:00
Jeong, YunWon
3734f32e42 Exception.set_traceback_typed (#5832) 2025-06-24 18:56:10 +09:00
Jeong, YunWon
a8c9703cbc classmethod copy attrs (#5831) 2025-06-24 17:23:37 +09:00
Jeong, YunWon
9c2a4695c1 Fix excepthook (#5830) 2025-06-24 16:58:27 +09:00
Jeong, YunWon
638d2183dc Fix builtins.dir (#5829) 2025-06-24 13:31:50 +09:00
Jeong, YunWon
86d8d23ad8 implement more property features (#5828)
* property getter_doc

* Fix property.isabstractmethod

* Fix property error messages

* refactor getter/setter/deleter

* fix property
2025-06-24 13:31:31 +09:00
Jeong, YunWon
3566dcab28 Pyfunction builtins and constructor (#5823)
* func builtins

* PyFunction constructor
2025-06-24 10:23:14 +09:00
Jeong, YunWon
0624ca622b functools.partial (#5825)
* functools.partial

* apply review
2025-06-23 23:53:57 +09:00
Ashwin Naren
70bcd78e85 Bump libffi (#5779)
Also remove system feature from jit libffi, as that forces libffi to be installed on the system
2025-06-23 23:28:33 +09:00
Jeong, YunWon
52301ddbe5 Fix PyFunction doc behavior (#5827) 2025-06-23 22:37:51 +09:00
Jeong, YunWon
9952c97edf BaseException.__setstate__ (#5821)
* docs

* BaseException.__setstate__
2025-06-23 13:49:59 +09:00
Jeong, YunWon
33af632914 fix maketrans (#5824) 2025-06-23 11:43:18 +09:00
Jeong YunWon
a288b77c4f BaseException.__setstate__ 2025-06-23 09:41:21 +09:00
Jeong, YunWon
0728da51fc binascii.hexlify (#5820) 2025-06-23 08:59:36 +09:00
Jeong, YunWon
b0b39194dd Fix cell_contents (#5822) 2025-06-23 08:59:07 +09:00
Jeong, YunWon
b1ecdf38b8 Fix bz2 pickle (#5819) 2025-06-23 08:55:59 +09:00
Lee Dogeon
dfc8fe018f Unmark fixed tests (#5818) 2025-06-23 08:55:28 +09:00
Jeong YunWon
0a6e1e80d7 docs 2025-06-23 01:05:54 +09:00
Jeong, YunWon
5d68313f91 sys.setswitchinterval (#5817) 2025-06-22 22:36:13 +09:00
Shahar Naveh
e27263aebd Convert new_X_error to use a macro (#5814) 2025-06-22 21:33:14 +09:00
Jeong YunWon
4cdb8d18b7 unicodedata.is_mirrored 2025-06-22 21:32:12 +09:00
Jeong YunWon
c824016301 Fix UnpackIterator constructor 2025-06-22 18:11:30 +09:00
Jeong YunWon
44d312419a Fix bytes constructor 2025-06-22 17:40:04 +09:00
Jeong, YunWon
3a54105e2c Fix struct tests (#5813) 2025-06-22 09:58:43 +09:00
Jeong, YunWon
7473a43fab Fix posix tests (#5811) 2025-06-22 09:58:29 +09:00
ShaharNaveh
f31ebf83ac Use raw string where appropriate. add newlines 2025-06-22 09:33:10 +09:00
ShaharNaveh
e6f5b6ad1a constify a bunch of methods 2025-06-22 09:33:10 +09:00
ShaharNaveh
55740b2277 Use any instead of loop 2025-06-22 09:33:10 +09:00
ShaharNaveh
e03f762b20 Make new method const 2025-06-22 09:33:10 +09:00
Jeong, YunWon
efc6b5dbaf Merge pull request #5809 from youknowone/fix-dict-unpack
Fix dict unpack
2025-06-22 09:08:06 +09:00
Jeong YunWon
d973e6da10 Apply copilot review 2025-06-22 00:01:22 +09:00
Jeong YunWon
3f1d39e5db Fix DictUpdate validation 2025-06-22 00:00:07 +09:00
Jeong YunWon
9467632e1b Fix dict unpacking order preservation 2025-06-21 23:51:37 +09:00
Jeong YunWon
829388b7b5 Edit only Rust files 2025-06-21 22:00:42 +09:00
Jeong, YunWon
f0e742714a Merge pull request #5808 from ShaharNaveh/cln-general 2025-06-21 13:25:33 +09:00
ShaharNaveh
fc68da7f6c newlines between functions 2025-06-21 00:50:40 +03:00
ShaharNaveh
eea33df6b1 Replace 255 with u8::MAX 2025-06-21 00:50:09 +03:00
ShaharNaveh
9574e14c0f Use .take instead of slicing a vec 2025-06-20 16:47:28 +03:00
ShaharNaveh
f2a6c09007 CLN: Loop with idx check location_tup len fields 2025-06-20 16:26:11 +03:00
Jeong YunWon
e4c8f2bb43 Fix ImportError 2025-06-20 16:12:30 +09:00
Jeong YunWon
d78ed67c26 fix instruction 2025-06-20 16:12:30 +09:00
Jeong YunWon
17bc8455aa Fix UnicodeDecodeError 2025-06-20 16:12:30 +09:00
Jeong YunWon
44d66dcdac Fix SyntaxError 2025-06-20 16:12:30 +09:00
Jeong YunWon
a186a5a9f5 Impl PyAttributeError args 2025-06-18 15:53:17 +09:00
Jeong YunWon
75531402e5 disable redox test to enable CI 2025-06-18 15:53:05 +09:00
largemouth
0dac02f443 chore: fix some typos in comment
Signed-off-by: largemouth <largemouth@aliyun.com>
2025-06-17 21:59:25 +09:00
Ashwin Naren
c968fe0fd9 remove macos skips 2025-06-15 17:36:13 +09:00
Ashwin Naren
125f14190a Remove unnecessary uv runin README (#5792)
* Update README.md

Remove unnecessary `uv run`

* uv comment

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2025-06-15 16:10:27 +09:00
Jeong YunWon
a6dd2d805b Skip test_local_unknown_cert to avoid CI failure 2025-06-15 16:04:37 +09:00
Jeong YunWon
6723bf30a7 Fix deque module name for test_repr 2025-06-15 16:04:37 +09:00
Jeong YunWon
2c61a12bed Apply coderabbit reviews 2025-06-15 16:03:46 +09:00
Jeong YunWon
f560b4cbfb Fix nightly clippy warnings 2025-06-15 16:03:46 +09:00
dependabot[bot]
4e094eaa55 Bump webpack-dev-server from 5.2.0 to 5.2.1 in /wasm/demo (#5801)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.2.0...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-06 23:17:32 +09:00
Jeong, YunWon
2e368baf2a Fix Nightly clippy (#5798) 2025-06-06 22:00:07 +09:00
Aneesh Durg
323ea3b96b Support incomplete parsing (#5764)
* continue accepting REPL input for multiline strings

* Match cpython behavior for all multi-line statements (execute when complete)

* Emit _IncompleteInputError when compiling with incomplete flag

* Refine when _IncompleteInputError is emitted

* Support multiline strings emitting _IncompleteInputError

* lint

* Undo accidental change to PyTabError

* match -> if let

* Fix test_baseexception and test_codeop

* fix spelling

* fix exception name

* Skip pickle test of _IncompleteInputError

* Use py3.15's codeop implementation

* Update Lib/test/test_baseexception.py

---------

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-06-05 14:41:56 +09:00
Noa
e27d03179f Remove getrandom 0.2 from dependencies 2025-05-21 12:12:14 +09:00
Jeong YunWon
81a9002ef2 Rename Dockerfile 2025-05-20 11:10:13 +09:00
Noa
18521290bf Update dependencies 2025-05-19 14:41:24 +09:00
Noa
5e682e3f17 Merge pull request #5788 from coolreader18/fix-prec
Fix panic with high precision
2025-05-16 20:28:25 -05:00
Noa
163296d306 Fix test_memoryio 2025-05-16 15:20:10 -05:00
Noa
1ae98ee177 Fix panic with high precision 2025-05-16 14:17:25 -05:00
Noa
2c02e2776b Fix warnings for rust 1.87 2025-05-16 18:52:14 +09:00
Ashwin Naren
72dc4954ad dev container update 2025-05-16 18:25:08 +09:00
Rex Ledesma
b696e56c5f chore: rely on the default inclusions for ruff 2025-05-15 19:15:51 +09:00
dependabot[bot]
d11d5c65e6 Bump streetsidesoftware/cspell-action in the github-actions group
Bumps the github-actions group with 1 update: [streetsidesoftware/cspell-action](https://github.com/streetsidesoftware/cspell-action).


Updates `streetsidesoftware/cspell-action` from 6 to 7
- [Release notes](https://github.com/streetsidesoftware/cspell-action/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: streetsidesoftware/cspell-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-13 04:08:12 +09:00
Ashwin Naren
5c0f70c361 add instructions 2025-05-12 14:24:10 +09:00
Rex Ledesma
4e2e0b41c6 chore: add ruff format --check (#5774)
* chore: add `ruff format --check`

* fix tests
2025-05-12 14:20:01 +09:00
Ashwin Naren
df380bca96 lzma FORMAT_ALONE implementation (#5777)
* implement init_alone

* error if format is raw and there is a memlimit
2025-05-09 13:26:28 +09:00
Ashwin Naren
9bd7f1810b Update README.md
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-05-08 14:59:46 +09:00
Ashwin Naren
31e6cca916 Update README.md 2025-05-08 14:59:46 +09:00
Rex Ledesma
b8095b84ff chore: allow uv run python -I ./whats_left.py 2025-05-08 14:51:52 +09:00
Rex Ledesma
908386091b feat: implement zlib.__version__ 2025-05-08 14:48:11 +09:00
Rex Ledesma
aee4c7ac69 chore: migrate settings to ruff.toml (#5773) 2025-05-08 14:46:57 +09:00
Rex Ledesma
dc75d203ff chore: upgrade to ruff==0.11.8 2025-05-08 14:11:43 +09:00
Jeong, YunWon
ce5524d72a Merge pull request #5769 from youknowone/radium-patch 2025-05-08 13:42:38 +09:00
Jeong YunWon
06c4b151d6 Add radium patch to fix CI 2025-05-07 18:21:36 +09:00
Jeong, YunWon
79646fd222 Merge pull request #5717 from arihant2math/lzma
lzma implementation
2025-05-07 18:00:06 +09:00
Jeong YunWon
d9c18c5593 flatten compression modules 2025-05-07 15:42:31 +09:00
Jeong YunWon
acae154f1b more diff-friendly disabling xz 2025-05-07 15:01:23 +09:00
Ashwin Naren
a5016446f4 _lzma implementation and test marking
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-05-07 15:01:23 +09:00
Ashwin Naren
2042d877f9 add lzma.py and test_lzma.py at 3.13.2
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-05-07 15:01:23 +09:00
Ashwin Naren
2a1ea45659 add _lzma module with new dependency
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-05-07 15:01:23 +09:00
Jeong YunWon
48b08a2b7f Revert "Split out common compression routines into separate file (#5728)"
This reverts commit 9c88475b31.
2025-05-07 15:01:09 +09:00
Noa
9c88475b31 Split out common compression routines into separate file (#5728) 2025-05-06 15:26:54 +09:00
Undersk0re
6aa80aa596 Update .gitignore, folder fix (#5762) 2025-05-06 14:07:12 +09:00
Karel Král
85f7ba51f4 Enable test_mtestfile 2025-05-02 03:39:46 +09:00
Jeong, YunWon
94b38a51c4 Fix build (#5765) 2025-05-01 15:38:50 +09:00
Hanif Ariffin
253cc4e846 Fix usize not using the same hash as PyInt when used as key into a di… (#5756)
* Fix usize not using the same hash as PyInt when used as key into a dictionary

* Fix test that unexpectedly succeed!

* Update extra_tests/snippets/builtin_object.py
2025-05-01 14:55:19 +09:00
Noa
431b900084 Merge pull request #5716 from arihant2math/codegen-panic-reduction
Cleaner panic output for codegen errors
2025-04-30 09:25:57 -05:00
Karel Král
301c32dba0 Increase numerical precision of log1p
Use Rust native ln_1p to increase numerical precision matching what is
expected by `test_mtestfile`.
2025-04-30 16:35:04 +09:00
Jeong YunWon
cd1c9be0e1 Fix Cargo.lock 2025-04-30 15:04:26 +09:00
Jeong, YunWon
6461a91933 More stdlib updates (#5737)
* update getopt to 3.13.3

* update getpass to 3.13.3

* update timeit to 3.13.3

* update reprlib to 3.13.3

* update fileinput to 3.13.3
2025-04-30 14:59:01 +09:00
Aneesh Durg
e49e743669 Support multiline function/class definitions in REPL and InteractiveConsole (#5743)
* Support multiline function/class definitions in REPL and InteractiveConsole
2025-04-30 14:53:37 +09:00
Ashwin Naren
e8df06582e bump dependencies 2025-04-30 14:49:21 +09:00
Jeong YunWon
02f120aaf4 copilot-instructions 2025-04-30 14:43:17 +09:00
Ashwin Naren
b84d6a36a6 update fileinput to 3.13.3 2025-04-30 14:33:52 +09:00
Ashwin Naren
d73f03b9ba update reprlib to 3.13.3 2025-04-30 14:33:52 +09:00
Ashwin Naren
1a4b035dac update timeit to 3.13.3 2025-04-30 14:33:52 +09:00
Ashwin Naren
dd40bf7566 update getpass to 3.13.3 2025-04-30 14:33:52 +09:00
Ashwin Naren
5e770e9c9e update getopt to 3.13.3 2025-04-30 14:33:52 +09:00
Jeong YunWon
d1b7dc551c skip flaky test_weakref 2025-04-30 14:32:43 +09:00
Jeong YunWon
b0991e28a2 Replace puruspe to pymath
Might be lower quality, but better compatibility
2025-04-29 17:39:50 +09:00
Noa
180746467e Merge pull request #5744 from coolreader18/upd-malachite
Update to malachite 0.6
2025-04-27 23:24:26 -05:00
Ashwin Naren
f55bf8f83b fix openssl error reasons (#5739) 2025-04-28 12:30:14 +09:00
Jeong, YunWon
ff10a64727 Fix test_poll::test_poll3 (#5718)
* test_poll3

* Refactor EventMask
2025-04-28 12:29:21 +09:00
Ashwin Naren
5561b6ead4 Re-add ssl feature to cron-ci.yaml for whats-left (#5750) 2025-04-28 12:20:42 +09:00
Ashwin Naren
392d1c04f6 More overlapped implementation (#5748) 2025-04-27 19:48:06 +09:00
Ashwin Naren
d46bcd9291 typing upgrade to 3.13.2 (#5590) 2025-04-27 15:26:37 +09:00
Ashwin Naren
ca496fb3b1 Collect whats_left data with sqlite feature
This fixes the output on the website, and correctly shows `_sqlite3` as implemented
2025-04-27 15:24:18 +09:00
Noa
7aad6e03e3 Update to malachite 0.6 2025-04-25 00:22:26 -05:00
Ashwin Naren
c97f4d1daf Failure marker (#5695)
* initial auto-upgrader

* platform support

* use ast walking

* detect testname automatically

* handle classes properly

* add instructions to fix_test.py
2025-04-24 13:28:38 +09:00
Jeong, YunWon
7d2a7a0e35 Merge pull request #5731 from arihant2math/pprint-313 2025-04-22 14:18:35 +09:00
Ashwin Naren
92e72aabdc update graphlib to 3.13.3 2025-04-21 21:21:10 -07:00
Ashwin Naren
8603cd9387 update heapq to 3.13.3 2025-04-21 21:12:12 -07:00
Ashwin Naren
1c64bde0ee update sched to 3.13.3 2025-04-21 21:11:42 -07:00
Ashwin Naren
70f3aec552 update linecache to 3.13.3 2025-04-21 21:11:42 -07:00
Ashwin Naren
6567d1d6ec update queue to 3.13.3 2025-04-21 20:48:13 -07:00
Ashwin Naren
a5214a0de7 update colorsys to 3.13.3 2025-04-21 20:45:06 -07:00
Ashwin Naren
a85a84330f update pprint to 3.13.3 2025-04-21 20:43:43 -07:00
Noa
494918d9fe Remove cfg_attr features for redox 2025-04-22 12:00:57 +09:00
Noa
3bfafb0ecb Merge pull request #5720 from youknowone/wasip2-build
basic wasip2 support
2025-04-21 13:39:41 -05:00
Reagan Bohan
ecbc6f7044 Fix mmap aborting with invalid fd in debug mode 2025-04-21 20:40:44 +09:00
Ashwin Naren
5ce860443c implement nt.getlogin
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-21 16:56:04 +09:00
Ashwin Naren
320d74527f update webbrowser and test_webbrowser to 3.13.3
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-21 16:54:00 +09:00
Ashwin Naren
82a62382d0 add wave at 3.13.2
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-21 08:48:38 +09:00
Noa
fbaeecc6a1 Merge pull request #5709 from coolreader18/bz2
Switch to `libbz2-rs-sys` and finish bz2 impl
2025-04-20 17:53:07 -05:00
Jeong YunWon
e434ff5f6e basic wasip2 support 2025-04-20 19:47:35 +09:00
Noa
f0d46bfeaa Finish _bz2 implementation 2025-04-19 23:13:37 -05:00
Noa
e377e43f05 Remove bz2 feature 2025-04-19 23:13:37 -05:00
Noa
e640487572 Use libbz2-rs-sys for bzip2 implementation
Co-authored-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-19 23:13:27 -05:00
Ashwin Naren
397a1968c8 fix clippy
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-19 11:15:01 -07:00
Ashwin Naren
783e45f8ac Apply review
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-04-19 09:48:20 -07:00
Ashwin Naren
fc331a154f fix errors and formatting
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-18 22:41:48 -07:00
Ashwin Naren
12ceb9695c cleaner panic output
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-18 22:31:29 -07:00
Hanif Ariffin
974c54ede2 Updated test_baseexception from 3.13.2 (#5638)
Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>
2025-04-19 11:08:50 +09:00
Noa
a4d1bba74e Update tarfile to 3.12.3 (#5714)
* Update tarfile to 3.12.3

* Unmark tests
2025-04-19 11:03:15 +09:00
Noa
960954eba5 Fix CI 2025-04-18 14:15:10 -05:00
Daniel Stuart
dabd93c255 Make stdio a feature (#5420) 2025-04-18 14:10:25 -05:00
Noa
0d4faa00a7 Merge pull request #5700 from arihant2math/match-cleanup
Cleanup match statement codegen
2025-04-18 13:56:57 -05:00
Ashwin Naren
fd665f6bb2 fix _suggestions module init
renames the module init from suggestions to _suggestions

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-18 12:19:40 +09:00
Noa
ecdb7d3229 Merge pull request #5706 from coolreader18/constant_time_eq
Use the `constant_time_eq` crate instead of our bespoke implementation
2025-04-17 11:55:34 -05:00
Noa
10fd02e42e Use constant_time_eq instead of our bespoke implementation 2025-04-17 11:05:57 -05:00
Noa
c53908fe9e Merge pull request #5657 from arihant2math/fix-readme
Remove broken badge from readme
2025-04-17 10:26:14 -05:00
dependabot[bot]
b72f3a4928 Bump http-proxy-middleware from 2.0.7 to 2.0.9 in /wasm/demo (#5708)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 20:23:32 +09:00
Ashwin Naren
55998c9e3b Merge pull request #5701 from arihant2math/more-tests
Add more independent tests at 3.13.3
2025-04-17 20:01:43 +09:00
Jeong, YunWon
e73b4e9734 Merge pull request #5705 from coolreader18/deps 2025-04-17 20:00:56 +09:00
Noa
2e14b7b5e8 Upgrade system-configuration and libz-rs-sys 2025-04-16 17:14:18 -05:00
Noa
7eb361c92f Upgrade which and rustix 2025-04-16 17:14:16 -05:00
Noa
2ca52bf3ba Merge pull request #5702 from arihant2math/struct-313
Update struct to 3.13.3 and update parts of test.support
2025-04-16 17:09:41 -05:00
Noa
2e260761ae Upgrade criterion 2025-04-16 17:09:07 -05:00
Noa
99a384a3c7 Upgrade radium 2025-04-16 17:08:52 -05:00
Ashwin Naren
662d3a1b4a fix test_zlib 2025-04-15 16:16:06 -07:00
Ashwin Naren
3a1a5d3cd0 fix test_unicode 2025-04-15 16:12:37 -07:00
Ashwin Naren
a0226df166 fix test_csv 2025-04-15 16:11:32 -07:00
Ashwin Naren
4336b9e787 fix test_decimal 2025-04-15 15:55:09 -07:00
Ashwin Naren
844090b0b8 update test_struct to 3.13.3 2025-04-15 14:48:20 -07:00
Ashwin Naren
213506d9ae update part of test.support.__init__ to 3.13.2 2025-04-15 14:48:09 -07:00
Ashwin Naren
4d53f5925c remove match test 2025-04-15 11:56:29 -07:00
Ashwin Naren
21272025c2 improve error handling 2025-04-15 11:54:46 -07:00
Ashwin Naren
d44324d4d0 clippy 2025-04-15 11:44:04 -07:00
Ashwin Naren
628287c14e update snapshot 2025-04-15 11:42:13 -07:00
Ashwin Naren
e949c9aa3f rename 2025-04-15 11:41:27 -07:00
Ashwin Naren
09c199a1ba match cleanup 2025-04-15 10:03:29 -07:00
Ashwin Naren
d47944b2fd error handling 2025-04-15 10:02:32 -07:00
Ashwin Naren
456e555e8b better error 2025-04-15 09:25:06 -07:00
Ashwin Naren
c7042fd847 remove unneeded validation 2025-04-15 08:39:58 -07:00
Ashwin Naren
a917da3b1a update test_float to 3.13.3 2025-04-15 16:24:10 +09:00
Ashwin Naren
fb0c4b6b3c add test_winapi 2025-04-15 16:24:10 +09:00
Ashwin Naren
49b348cc7e Remove Instruction::IsOperation 2025-04-14 22:33:55 -07:00
Ashwin Naren
a7ad848270 Rust dependency updates (#5651) 2025-04-15 08:50:29 +09:00
Ashwin Naren
c2c20758c9 Remove imp (#5693) 2025-04-14 16:56:22 +09:00
Ashwin Naren
c7df344805 update calendar and test_calendar to 3.13.2 2025-04-14 16:55:00 +09:00
Ashwin Naren
4094c5bfc9 minor mark 2025-04-14 16:54:06 +09:00
Jeong, YunWon
4ae2936a45 fix more cspell warnings (#5689) 2025-04-11 12:08:07 +09:00
Ashwin Naren
fd2764c7c7 Remove asynchat and asyncore (#5688)
* try to remove asynchat and asyncore

* remove associated tests

* temp patch of test_ftplib
2025-04-11 11:11:12 +09:00
Ashwin Naren
b81ae9b954 More cspell fixes (#5670) 2025-04-11 09:37:20 +09:00
Ashwin Naren
d96374faba Add _pyrepl (#5540)
---------

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-04-11 09:30:58 +09:00
Ashwin Naren
02533ace81 remove nntplib 2025-04-11 09:28:43 +09:00
Ashwin Naren
22333e755b remove svg 2025-04-11 09:27:00 +09:00
Ashwin Naren
8dc1718002 Match statements rewrite (#5628) 2025-04-10 14:00:54 +09:00
Ashwin Naren
ad5ffb648f Remove packaging from release (#5680) 2025-04-08 13:37:47 +09:00
Ashwin Naren
861055f558 Add nt constants (#5676) 2025-04-06 17:23:56 +09:00
Ashwin Naren
3c6bc2cf9f Add _suggestions module (#5675) 2025-04-06 17:22:26 +09:00
Ashwin Naren
be56911598 _tkinter pt. 2 (#5640) 2025-04-06 17:21:28 +09:00
Noa
98137eb79c Switch to const-initialized thread_local variables where appropriate 2025-04-05 19:59:43 +09:00
Hanif Ariffin
2230d6c751 Fix not throwing the same error as CPython in test_pathlib.test_expanduser (#5578)
* Fix not throwing the same error as CPython when trying to expanduser of a non-existent user

Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>

* add pwd test

* Skip pwd test on windows

---------

Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>
Co-authored-by: Jeong YunWon <jeong@youknowone.org>
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-04-05 15:33:33 +09:00
Hanif Ariffin
d800a6bb98 Update test_math from CPython 3.13.2 (#5610)
Implemnted fma in math module.
2025-04-05 14:53:40 +09:00
Jeong, YunWon
e0a35e4322 Merge pull request #5661 from arihant2math/doc
Fix doc warnings
2025-04-05 14:49:41 +09:00
dependabot[bot]
c2665e38ba Bump openssl from 0.10.71 to 0.10.72
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.71 to 0.10.72.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.71...openssl-v0.10.72)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.72
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-05 14:48:08 +09:00
Ashwin Naren
ab4dffb53c this should just fail if warnings happen because of RUSTFLAGS 2025-04-04 11:37:40 -07:00
Jeong YunWon
36cce6b174 run fmt 2025-04-05 00:14:03 +09:00
Ashwin Naren
5c854fc690 clear out warnings 2025-04-04 21:46:28 +09:00
Ashwin Naren
883e0cab29 build docs on CI and deny warnings 2025-04-04 21:46:09 +09:00
Jeong YunWon
d7113e11db Fix more cspell warnings 2025-04-04 21:45:03 +09:00
Jeong, YunWon
66cf905e8b Merge pull request #5668 from youknowone/cspell
update  cspell dicts and ci order
2025-04-04 16:41:40 +09:00
Jeong YunWon
7ac61f3840 fix cspell warnings 2025-04-04 16:15:54 +09:00
Jeong YunWon
2c94b809ae move cspell to last step 2025-04-04 16:09:51 +09:00
Jeong, YunWon
d52081fe41 Merge pull request #5663 from arihant2math/remove-dep-modules 2025-04-04 16:04:58 +09:00
Ashwin Naren
e7f04612f6 remove chunk.py 2025-04-04 16:03:48 +09:00
Ashwin Naren
fd4ad3e4d1 Remove smtpd 2025-04-04 16:03:31 +09:00
Ashwin Naren
f1d45ee5a7 remove unused deprecated libraries 2025-04-03 22:12:54 -07:00
Ashwin Naren
6620aa07af update email to 3.13.2 2025-04-03 22:12:54 -07:00
Ashwin Naren
8063148598 Fix clippy lints from rust 1.86 update (#5665)
* handle rust 1.86 update

* fix windows clippy lint

* disable cspell under jit/instruction

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2025-04-04 14:04:13 +09:00
Ashwin Naren
2bf2332806 Cleanup whats_left.py (#5654)
* cleanup whats_left.py

* add features flag
2025-04-02 16:31:25 +09:00
Ashwin Naren
64a0721616 Remove broken badge from readme 2025-04-01 08:08:16 -07:00
dependabot[bot]
c3ed002b12 Bump streetsidesoftware/cspell-action from 2 to 6 in the github-actions group (#5646)
* Bump streetsidesoftware/cspell-action in the github-actions group

Bumps the github-actions group with 1 update: [streetsidesoftware/cspell-action](https://github.com/streetsidesoftware/cspell-action).


Updates `streetsidesoftware/cspell-action` from 2 to 6
- [Release notes](https://github.com/streetsidesoftware/cspell-action/releases)
- [Changelog](https://github.com/streetsidesoftware/cspell-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/streetsidesoftware/cspell-action/compare/v2...v6)

---
updated-dependencies:
- dependency-name: streetsidesoftware/cspell-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update .github/workflows/ci.yaml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-04-01 10:31:07 +09:00
Ashwin Naren
f6a9754b4e Remove telnetlib deprecated in 3.13 (#5649) 2025-04-01 10:19:39 +09:00
Ashwin Naren
264f3d792a remove xdrlib deprecated in 3.13 (#5648) 2025-04-01 10:18:42 +09:00
Ashwin Naren
cb967c697b Fix release CI (#5647) 2025-04-01 10:17:29 +09:00
Noa
e21a04cf4b Merge pull request #5635 from hbina/hbina-fix-py313-test-bool
Fixed an expected failure in the behavior of negating a bool argument
2025-03-31 11:04:07 -05:00
Hanif Ariffin
f0bcad7116 Added test_audit
This test is currently noop because RustPython does not have sys.audit?

Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>
2025-03-31 18:49:08 +09:00
Jeong YunWon
57a83db69d try IncrementalNewlineDecoder in doctest 2025-03-31 18:00:36 +09:00
ivan-shrimp
3ad8fd711f fix expression list order
don't emit a no-op when unpacking a single element

assume positional args stored as tuple in extended call
2025-03-31 18:00:19 +09:00
Noa
160363fa46 Fix float parsing (#5643)
* Fix float parsing

* Add rustpython_literal::complex

* Don't call .to_string() on a constant
2025-03-31 14:37:47 +09:00
Noa
0b35946972 Make FromArgs default field take an expression, not a string literal 2025-03-31 11:48:06 +09:00
Noa
24d995678f Remove some unncessary dependencies 2025-03-31 11:28:15 +09:00
Hanif Ariffin
8e7039405e Fix some clippy issues
Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>
2025-03-30 23:53:20 +08:00
Hanif Ariffin
8f989e4a67 Fixed an expected failure in the behavior of negating a bool argument
Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>
2025-03-30 23:01:20 +08:00
Jeong, YunWon
696dceacdc Merge pull request #5255 from youknowone/lib-socket
Update socket and test from CPython 3.12.2
2025-03-30 14:47:30 +09:00
Jeong YunWon
9e2f6bd187 mark failing tests of test_socket 2025-03-30 14:20:17 +09:00
Jeong YunWon
b620f03728 Update socket from CPython 3.12.2 2025-03-30 13:55:55 +09:00
Jeong, YunWon
ade45c2312 Merge pull request #5391 from JazzGlobal/4982-AddMissingFieldsToPyStructTime 2025-03-30 13:44:37 +09:00
Noa
b067986576 Bump cranelift to 0.118 2025-03-30 13:40:24 +09:00
Jeong YunWon
763ba9fd6a edit test_time 2025-03-29 09:46:47 +09:00
Jeong YunWon
fd270775a3 time._STRUCT_TM_ITEMS 2025-03-29 09:46:47 +09:00
Jeong YunWon
b99e7f62b2 Add pystruct(skip) 2025-03-29 09:46:47 +09:00
Christopher Gambrell
bb8606dbed implement tm_gmtoff and tm_zone 2025-03-29 09:46:47 +09:00
Jeong YunWon
0abd8b1d87 Fix pystructseq 2025-03-29 09:46:47 +09:00
Jeong, YunWon
58a17f337d Merge pull request #5633 from coolreader18/compiler-deps 2025-03-28 13:45:00 +09:00
Noa
d3d92bbb6f Update unparse to work with ruff & remove ruff_python_codegen 2025-03-27 22:14:58 -05:00
Noa
8081e0d281 Copy unparse.rs from rustpython-parser 2025-03-27 22:09:00 -05:00
Noa
f398321b1f Remove parser dependency from codegen 2025-03-27 22:09:00 -05:00
Noa
7d05f881c4 Have rustpython_literal::escape support wtf8 2025-03-28 11:26:29 +09:00
Noa
030243a6f9 Split out wtf8 into its own crate 2025-03-28 11:26:29 +09:00
Noa
6b72d2ef5d Check+lint examples, tests, and benches in CI 2025-03-28 11:26:12 +09:00
Noa
b6aacbf401 Merge pull request #5629 from coolreader18/surrogate-literals
Parse surrogates in string literals properly
2025-03-27 10:15:41 -05:00
Noa
dd467f6c73 Update common/src/wtf8/mod.rs
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-03-27 10:15:18 -05:00
Jeong, YunWon
cd89aa51f0 Fix _ctypes.Array base and metaclass (#5620) 2025-03-27 19:45:04 +09:00
Jeong, YunWon
f27c1f7ea3 Merge pull request #5624 from youknowone/libffi-workspace
common dependency in workspace
2025-03-27 14:51:47 +09:00
Jeong, YunWon
c7ca173c90 Merge pull request #5254 from youknowone/exception-group
ExceptionGroup
2025-03-27 14:51:12 +09:00
Noa
c9161c02b6 Merge pull request #5625 from youknowone/clippy
Apply nightly clippy suggestions
2025-03-26 23:40:42 -05:00
Noa
6e79a2aa8a Merge pull request #5626 from youknowone/remove-unused-deps
Remove unused dependency
2025-03-26 23:37:52 -05:00
Noa
bea25a0285 Merge pull request #5627 from youknowone/once-cell
Replace direct use of once_cell to std
2025-03-26 23:36:31 -05:00
Jeong, YunWon
c96fd3d900 Merge pull request #4700 from youknowone/cspell
Activate cspell lint
2025-03-27 13:25:08 +09:00
Noa
0a07cd931f Fix more surrogate crashes 2025-03-26 23:12:21 -05:00
Noa
c6cab4c43a Parse surrogates in string literals properly 2025-03-26 22:44:42 -05:00
Noa
2ab8716c95 Merge pull request #5623 from coolreader18/refactor-codecs
Refactor codecs
2025-03-26 14:01:26 -05:00
Noa
e3d96aa3ca Remove commented-out code
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-03-26 12:42:03 -05:00
Jeong YunWon
10d2837041 rework dicts 2025-03-27 02:15:03 +09:00
Jeong YunWon
372e683063 disable cspell from files with a bunch of OS jargons 2025-03-27 01:47:17 +09:00
Jeong YunWon
5f6f6cce92 add cspell to CI 2025-03-27 01:30:09 +09:00
Jeong YunWon
27bcba3027 wasm test prints more info 2025-03-27 01:28:36 +09:00
Jeong YunWon
053583f5a0 Add wasm/demo/.envrc 2025-03-27 01:28:36 +09:00
Jeong YunWon
5e0eace8d9 test_exception_group from CPython 3.12.2 2025-03-27 01:28:36 +09:00
Jeong YunWon
e7fdfca5f5 Add python-implemented ExceptionGroup 2025-03-27 01:28:36 +09:00
Jeong YunWon
2d4eec88d3 better webdriver error printing 2025-03-27 01:28:34 +09:00
Jeong YunWon
7f94c10be7 patch typing not to reqiure contextlib
This will be correctly fixed in future CPython
2025-03-27 01:20:23 +09:00
Jeong YunWon
549cce24c8 Add #[pystruct(skip)] 2025-03-26 22:40:43 +09:00
Jeong YunWon
97fa11d526 Remove unused dependency 2025-03-26 20:18:29 +09:00
Jeong YunWon
ad5788589b Remove more direct use of OnceCell 2025-03-26 18:22:23 +09:00
Jeong YunWon
ec09599d84 Remoce once_cell::Lazy usage 2025-03-26 18:22:23 +09:00
Noa
f323d14ed3 Refactor codecs 2025-03-26 02:24:01 -05:00
Jeong YunWon
bc38e9dedd Apply nightly clippy suggestions 2025-03-26 14:52:23 +09:00
Noa
7ac90f5cbc Merge pull request #5587 from coolreader18/wtf8
Allow surrogates in str
2025-03-25 21:08:01 -05:00
Noa
f3b8d5515a Address comments 2025-03-25 21:06:56 -05:00
Noa
bd55baefa6 Optimize Wtf8Codepoints::count 2025-03-25 19:05:12 -05:00
Noa
a86126419c Fix remaining tests 2025-03-25 19:05:12 -05:00
Noa
5c22697344 Implement fsencode/fsdecode for FsPath 2025-03-25 19:05:12 -05:00
Noa
cc6f3d3051 Make TextIOWrapper wtf8-compatible 2025-03-25 19:05:12 -05:00
Noa
b36b32bfe8 Make re wtf8-compatible 2025-03-25 19:05:12 -05:00
Noa
3945d3b2fe Make format wtf8-compatible 2025-03-25 19:05:12 -05:00
Noa
ba1b5811ee Update encoding to use wtf8 2025-03-25 19:05:11 -05:00
Noa
7f4582bb23 Make cformat wtf8-compatible 2025-03-25 19:05:11 -05:00
Noa
cace112b1a Allow surrogates in str 2025-03-25 19:05:11 -05:00
Noa
e3a1031081 Refactor cformat in prep for wtf8 2025-03-26 08:36:16 +09:00
Jeong, YunWon
2a41072b44 Windows installer via cargo-packager (#5549)
* cargo-packager config

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>

* add templates

* update release.yml

* update wix installer config

---------

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-03-25 10:27:45 +09:00
Ashwin Naren
01d470ff77 _ctypes pt. 4 (#5582)
* correct error type when symbol is not found

* restype get/set

* base of PyCSimple for PyCArray

* PyCSimple::to_arg

* par down ctypes

* nt._LOAD_LIBRARY_SEARCH_DEFAULT_DIRS

* arguments for ctypes function

* force failure to import ctypes
2025-03-25 09:09:29 +09:00
Ashwin Naren
9779de98b8 _tkinter pt. 1 (#5583)
* Add _tkinter module and gate

* add tkinter module

* add tcl_error

* fix tk setup and add demo

* fix TK_VERSION

* create and TkApp
2025-03-23 12:25:52 +09:00
Noa
c585678ec9 Merge format and literal back into this repo (and update malachite) (#5618)
* Merge format and literal back into this repo

* Update format and literal to work

* Update malachite

* Remove RustPython/Parser from Cargo.toml
2025-03-23 09:27:13 +09:00
Jeong, YunWon
eaf4cdf5e1 Update to ruff_python_parser 0.11 (#5615)
* Update to ruff_python_parser 0.11

* Try fix windows long paths error
2025-03-23 08:28:29 +09:00
Noa
948368fdb4 Try fix windows long paths error 2025-03-20 11:29:49 -05:00
0717d5a331 remove wrong cpython_only tag 2025-03-20 14:35:04 +09:00
a9bfaa96c5 remove unnecessary cpython_only 2025-03-20 14:35:04 +09:00
70a5774737 Update CPYTHON_SPECIFIC_MODS to include '_testlimitedcapi' 2025-03-20 14:35:04 +09:00
2d3b125d51 Add expectedFailure and skip decorators for RUSTPYTHON tests in test_class.py 2025-03-20 14:35:04 +09:00
4081c08b5a add cpython_only tag for tests required _testlimitedcapi 2025-03-20 14:35:04 +09:00
70c36a48a8 Copy test_class.py from cpython v3.13.2 2025-03-20 14:35:04 +09:00
Ashwin Naren
37bd49cf38 add _pylong.py at 3.13.2 2025-03-20 14:33:54 +09:00
Ashwin Naren
081dc4e8ca removed cgib (#5609) 2025-03-20 14:28:53 +09:00
Ashwin Naren
a4466adf8b add _aix_support 2025-03-20 14:28:26 +09:00
Noa
bfe72689fc Remove -merge from Cargo.lock gitattributes 2025-03-20 14:28:03 +09:00
Noa
950a8d5694 Update to ruff_python_parser 0.11 2025-03-19 22:45:57 -05:00
Stefan Lukas
a6b4ef7f5d Replace Python parser with ruff parser (#5494)
* stage1

* compiler pass build

* introduce rustpython-compiler-source

* stage2

* fixup

* pass compile

* Fix hello world compiler test

* Fix code generation for if-elif-else statement

* Fix code generation for lambda expression

* Fix code generation for integers

* Fix code generation for fstrings

* Fix code generation for if statement

* Fix code generation for if statement

* Fix code generation for if statement

* Fix code generation for fstring

* Fix code generation for class definition

* Replace feature flags

* Initialize frozen core modules

* Allow __future__ import after module doc comment

* Disable ast module

* Commit remaining fixes for compile errors in examples

* Fix some warnings

* Update ast stdlib module

* Update ast stdlib module

* Update ast stdlib module

* Update ast stdlib module

* Update ast stdlib module

* Split ast stdlib module into files

* Fix codegen for positional arguments with defaults

* Update ast stdlib module

* Update ast stdlib module

* Extract string and constant handling from expression.rs

* Always add required fields to AST nodes

* Compile doc strings correctly again

* Enable "ast" Cargo feature by default

* Refactor compilation of big integer literal

* Update ast stdlib module

* Update ast stdlib module

* Update ast stdlib module

* Reset barebones example

* Fix some left-over warnings

* Undo accidential change

* Adapt shell to ruff parser

* Pin parser to v0.4.10

* fix clippy

* Add TODO about interactive mode

* Fix compilation of complex number expression

* Remove moved code

* Update test case to ruff v0.4.10

* Apply suggestion

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>

* Apply suggestion

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>

* Fix compilation of fstring expression

* Fix compilation of fstring expression

* Fix wasm compile errors

* Attach correct source locations to ast objects

* Fix some more wasm compile errors

* Consider compile mode and enable AST stdlib module again

* Fix incorrect AST source location end column

* Fix compile error if "compiler" feature is not enabled

* Fix regrtests

* Fix some test_ast tests

* Add source range to type ignore

* Fix incompatibility with Rust 2024 edition

* Fix todos by implementing missing ast conversions and deleting unused code

* Appease clippy

* Fix remaining ast tests

* Fix remaining ast tests

* Mark/fix remaining tests

* Fix more

* Hacky windows fix

---------

Co-authored-by: Kangzhi Shi <shikangzhi@gmail.com>
Co-authored-by: Jeong YunWon <jeong@youknowone.org>
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
Co-authored-by: Noa <coolreader18@gmail.com>
2025-03-19 21:06:03 -05:00
Nicholas Paulick
45c0fa0e77 Floating Point Power and While Loop JIT (#5614)
* Initial commit for power function

* Float power jit developed

* Addded support for Floats and Ints

* Integration Testing for JITPower implementation.

* Update instructions.rs

* Update int_tests.rs

* Update float_tests.rs

* Updated cranelift and power function to use magic bs

* Updated the compile_fpow accuracy

* updating while loop test

* Update instructions.rs

cranelift more like painlift

* Update instructions.rs

* Update instructions.rs

* initial commit for making stable PR ready features

* fixed final edge case for compile_ipow

* fixed final edge case for compile_ipow

* commenting out compile_ipow

* fixed spelling errors

* removed unused tests

* forgot to run clippy

* Cleaned the branch

* While loop implementation for Cranelift 0.116.1

* Floating Point

* Removed testing print statement

* Resolved some formatting

* Fixed cargo fmt warning

* Fixed int div and int exp issues

* Fixed formatting

---------

Co-authored-by: Nick <nick@Samanthas-MBP.wi.rr.com>
Co-authored-by: JoeLoparco <loparcojoseph@gmail.com>
Co-authored-by: Daniel O'Hear <149127239+dohear@users.noreply.github.com>
Co-authored-by: Joseph Loparco <149088810+JoeLoparco@users.noreply.github.com>
Co-authored-by: Nick <nick@pcp090057pcs.mu.edu>
Co-authored-by: dohear <daniel.ohear@marquette.edu>
Co-authored-by: Nathan Rusch <nathan.rusch@icloud.com>
Co-authored-by: Nick <nick@pcp093574pcs.mu.edu>
Co-authored-by: Joseph Loparco <--global loparcoJoseph@gmail.com>
Co-authored-by: Nick <nick@Samanthas-MacBook-Pro.local>
2025-03-19 13:50:42 -05:00
Noa
a596568151 Use lexopt for argument parsing (#5602) 2025-03-19 12:28:58 -05:00
Jeong, YunWon
bd94d8d50c Remove uu.py and test_uu.py (#5607)
* Remove uu.py and test_uu.py

* patch email.message
2025-03-14 11:42:26 +09:00
Ashwin Naren
7fab64ed9c Revert "Update statistics to 3.13.2 (#5592)" (#5606)
This reverts commit ff970b0e1c.
2025-03-14 11:41:14 +09:00
Ashwin Naren
8e22c399df partially fix sys.getwindowsversion() (#5595) 2025-03-14 11:38:35 +09:00
Ashwin Naren
7546ea91a9 patch email.message 2025-03-13 10:16:51 -07:00
Ashwin Naren
8da66978bf Remove uu.py and test_uu.py 2025-03-13 09:49:10 -07:00
Ashwin Naren
8484bfa2e0 Remove cgi module (#5597)
* no cgi

* mark failing test
2025-03-12 09:47:34 +09:00
Ashwin Naren
ff970b0e1c Update statistics to 3.13.2 (#5592)
* statistics to 3.13.2

* set flaky test
2025-03-11 22:37:26 +09:00
Jeong, YunWon
8be7e4327d Merge pull request #5596 from arihant2math/osx-support-313
_osx_support update to 3.13.2
2025-03-11 15:58:19 +09:00
Jeong, YunWon
82eeb237dc Merge pull request #5598 from arihant2math/fix-whats-left-again
Fixed whats left
2025-03-11 15:57:57 +09:00
Ashwin Naren
cbbadf562f Fixed whats left 2025-03-10 23:27:05 -07:00
Ashwin Naren
4308321f39 _osx_support update to 3.13 2025-03-10 23:13:32 -07:00
Jeong, YunWon
985eebf9b0 Merge pull request #5589 from youknowone/pyattr-const
Replace pyattr(once) to constant
2025-03-11 10:12:50 +09:00
Ashwin Naren
bf28152a32 add os support modules 2025-03-10 11:43:53 +09:00
Ashwin Naren
bae0ad3aeb Fix extra newline in module.csv generation in cron ci (#5591) 2025-03-10 11:43:26 +09:00
Jeong YunWon
87fae150da Replace pyattr(once) to constant 2025-03-09 12:39:12 +09:00
Ashwin Naren
97853bf0f1 Fix module.csv generation in cron ci (#5586) 2025-03-06 13:20:04 +09:00
Noa
cc0a1ce9e2 Update webpack (#5585)
* Update webpack

* Build demo before notebook

* Use with instead of env for actions-gh-pages
2025-03-05 16:15:14 -06:00
Daniel O'Hear
58ebf04bac Add JIT compilation support for integer multiplication, division, and exponents (#5561)
* Initial commit for power function

* Float power jit developed

* Addded support for Floats and Ints

* Integration Testing for JITPower implementation.

* Update instructions.rs

cranelift more like painlift

* Update instructions.rs

* Update instructions.rs

* initial commit for making stable PR ready features

* fixed final edge case for compile_ipow

* fixed final edge case for compile_ipow

* commenting out compile_ipow

* fixed spelling errors

* removed unused tests

* forgot to run clippy

---------

Co-authored-by: Nicholas Paulick <paulicknicholas@gmail.com>
Co-authored-by: Nick <nick@Samanthas-MBP.wi.rr.com>
Co-authored-by: JoeLoparco <loparcojoseph@gmail.com>
Co-authored-by: Nathan Rusch <nathan.rusch@icloud.com>
Co-authored-by: dohear <daniel.ohear@marquette.edu>
2025-03-05 14:41:45 -06:00
Ashwin Naren
7fea1e1b4a fix what is left data upload to website and trigger cron-ci on workflow update 2025-03-05 13:50:52 -06:00
Ashwin Naren
d2bf31724f fix clippy 2025-03-05 13:49:37 -06:00
Ashwin Naren
b4929d258d formatting 2025-03-05 13:49:37 -06:00
Ashwin Naren
ddf2e591c6 resolve comments 2025-03-05 13:49:37 -06:00
Ashwin Naren
33940726a8 upgrade to windows-sys 0.59.0 2025-03-05 13:49:37 -06:00
Ashwin Naren
05cb8c0b73 Update socket.rs
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2025-03-05 13:49:37 -06:00
Ashwin Naren
8d2c6807d2 fix non-windows build 2025-03-05 13:49:37 -06:00
Ashwin Naren
4d9804f188 formatting 2025-03-05 13:49:37 -06:00
Ashwin Naren
3ae1160868 Remove winapi dependency 2025-03-05 13:49:37 -06:00
Ashwin Naren
6804dd4363 use rust-toolchain targets options instead of using rustup 2025-03-05 13:45:31 -06:00
Ashwin Naren
defcadafbb publish demo on weekly release 2025-03-05 13:45:31 -06:00
Ashwin Naren
40e3f49ab7 _ctypes pt. 3 (#5530)
* Initial CFuncPtr implementation

* function calling via libffi

* working no-arg function call

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-03-03 15:53:04 +09:00
Ashwin Naren
56196890f5 Actions caching for nodejs (#5575)
* caching for nodejs and various CI dependency updates

* commit the package-lock.json
2025-03-02 18:18:17 +09:00
Noa
6daee1b00e Warn on elided_lifetimes_in_paths 2025-03-01 13:49:33 +09:00
Ashwin Naren
8ff856d7ce _ctypes addressof and Structure (#5573)
* _ctypes.addressof

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>

* ctypes.Structure implementation

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>

* clippy fix

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>

* formatting

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>

---------

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-03-01 13:48:28 +09:00
Ashwin Naren
6731c4b1ab fix devcontainer.json 2025-02-28 13:50:10 -06:00
Ashwin Naren
544182ebfc update license dates 2025-02-27 19:10:56 -06:00
Ashwin Naren
12c3fa0b87 update wix installer config 2025-02-27 08:48:22 -08:00
Ashwin Naren
aa4774fe32 update release.yml 2025-02-27 08:44:42 -08:00
Ashwin Naren
26bc4ba3dd add templates 2025-02-26 22:39:00 -08:00
Noa
b2abb1af84 Remove redundant lints now that we're on edition2024 2025-02-26 23:46:57 -06:00
Ashwin Naren
cebbca9e63 cargo-packager config
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-26 20:59:10 -08:00
Jeong, YunWon
1a2dda502a Merge pull request #5560 from arihant2math/2024-migrate
Migrate to the 2024 edition
2025-02-27 12:22:03 +09:00
Ashwin Naren
b870b0e1b5 2024 edition formatting
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-26 11:48:22 -08:00
Ashwin Naren
df2354fdb7 migrate to the 2024 edition
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-26 11:47:32 -08:00
Jeong, YunWon
c20c90fbc4 Merge pull request #5563 from coolreader18/fix-zlib-tests
Fix a bunch of zlib tests & update gzip.py to Python 3.13
2025-02-26 15:47:52 +09:00
CPython Developers
aba3d5c082 Update gzip,test_gzip from CPython 3.13 2025-02-26 00:10:24 -06:00
Noa
8c5602f2fb Fix a bunch of zlib tests 2025-02-26 00:10:24 -06:00
Noa
4468dcbe34 Switch to libz-rs-sys for zlib implementation 2025-02-25 23:19:19 -06:00
Ashwin Naren
235adafa0b tests
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-25 17:19:53 +09:00
Noa
864e8598f8 Enable rust2024-incompatible pat and keyword-ident lints 2025-02-25 00:32:02 -06:00
Jeong, YunWon
f426348a94 Merge pull request #5558 from coolreader18/openssl-probe-no-env
Use non-env-var methods from openssl_probe
2025-02-25 14:54:19 +09:00
Noa
085ac88438 Use non-env-var methods from openssl_probe 2025-02-24 23:10:07 -06:00
Noa
4881f61be6 Mark env::{set,remove}_var() unsafe 2025-02-24 23:02:54 -06:00
Jeong, YunWon
ff9947ff14 Enable unsafe_op_in_unsafe_fn and missing_unsafe_on_extern lints (#5557)
* Enable unsafe_op_in_unsafe_fn lint

* Enable missing_unsafe_on_extern lint

* Make PyObjectRef::{from,into}_raw() use NonNull
2025-02-25 13:42:25 +09:00
Noa
92e02a7f79 Make PyObjectRef::{from,into}_raw() use NonNull 2025-02-24 21:25:23 -06:00
Noa
0a8b0406f5 Enable missing_unsafe_on_extern lint 2025-02-24 21:25:23 -06:00
Noa
1c3b198a17 Enable unsafe_op_in_unsafe_fn lint 2025-02-24 21:25:23 -06:00
Noa
52208b3c90 Update to syn2 (#5556) 2025-02-25 11:54:13 +09:00
Noa
2721f2de3f Fix a bunch of random tests (#5533) 2025-02-25 08:41:54 +09:00
Ashwin Naren
b55a55afc7 update the csv with the temp data for website what's left
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-24 11:30:27 -06:00
Ashwin Naren
d7a72b5755 add constants and implement functions
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-24 16:16:34 +09:00
Ashwin Naren
1f3a9672c3 Add _winapi.GetACP and enable test_unicode on windows (#5547)
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-24 13:21:02 +09:00
Axect
31c5c3eb9d Update puruspe version to 0.4.0
To resolve the issue (#5496)
2025-02-24 11:15:33 +09:00
Ashwin Naren
7fada8b97e fix _ctypes error names
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-23 16:07:52 +09:00
Ashwin Naren
429754fd33 Fix unicode decode bug on surrogate error mode (#5546)
* subtract with overflow to check for whether to use surrogate

* enable test_argparse for windows on ci

------

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-23 16:07:22 +09:00
Ashwin Naren
b4f0a589ed platform-dependent Windows testing (#5536)
* disable test_argparse on windows

* fix test_exceptions and mark it as platform dependent

* test importlib on windows

* explain why windows tests fail

* mark test_argparse as non platform-independent

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-23 09:48:02 +09:00
Noa
331a3c108f Switch to criterion in sre_engine benchmarks 2025-02-23 09:44:57 +09:00
Ashwin Naren
d698b28ce5 Ensure pymethod cannot be both magic and named simultaneously + macro documentation (#5538)
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-22 17:22:31 +09:00
Ashwin Naren
23236aa8c7 test_datetime now works on windows
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-21 15:09:40 +09:00
Noa
a9331bb34d Fix warnings for rust 1.85 2025-02-20 14:58:59 -06:00
Hanif Ariffin
65dcf1ce1c Updating test_math.py to CPython 3.12.9 (#5507)
* Fixed implementation against CPython 3.12.9 Lib/test/test_math.py tests
---------

Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>
Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2025-02-20 11:21:12 +09:00
Ashwin Naren
e2b0fe4266 _ctypes pt. 2 (#5524)
* add __version__

* add more types/constants

* shared library and ExternalLibs implementation

* FreeLibrary for windows

* fixed PyCSimple

* LoadLibrary and FreeLibrary for non-windows

* fault-tolerant float equality

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-20 10:50:10 +09:00
Noa
fa2acd7cde Update rand to 0.9 2025-02-18 17:07:26 +09:00
Ashwin Naren
a71c16f8cb test colorize on ci
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-18 16:52:04 +09:00
Ashwin Naren
f466971312 clippy
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-18 15:50:27 +09:00
Ashwin Naren
69b1a9910f formatting
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-18 15:50:27 +09:00
Ashwin Naren
4ed735b424 time.daylight for windows
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-18 15:50:27 +09:00
Ashwin Naren
175afd97d8 time.timezone for windows
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-18 15:50:27 +09:00
Ashwin Naren
72338d578b tzname on windows
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-18 15:50:27 +09:00
Ashwin Naren
9856d94f2d function to retrieve tz info on windows
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-18 15:50:27 +09:00
Ashwin Naren
517ffed401 fix clippy lint
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-17 14:15:57 -06:00
Ashwin Naren
38a6a8d984 duplicate windows-sys
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-17 14:15:57 -06:00
Ashwin Naren
630c1ff8d1 simple part of the bump
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-17 14:15:57 -06:00
Ashwin Naren
7e1568a1ff Revert "windows-rs upgrade to 0.59"
This reverts commit 547530724e77a592734d8cd396115c4124d7a9f9.
2025-02-17 14:15:57 -06:00
Ashwin Naren
6788010f7d windows-rs upgrade to 0.59 2025-02-17 14:15:57 -06:00
Ashwin Naren
9e310934d3 fix panic
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-17 12:00:39 -06:00
Ashwin Naren
e8a3406624 itertools upgrade 2025-02-16 10:20:56 +09:00
Ashwin Naren
fde87a340c Initial _ctypes implementation (#5519)
* initial _ctypes implementation with _CData, get_errno, and set_errno

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-15 16:03:58 +09:00
Jeong, YunWon
19050afc3f Merge pull request #5520 from arihant2math/colorize
Add _colorize at 3.13.2
2025-02-14 15:34:26 +09:00
Ashwin Naren
e96557b3e1 add _colorize.py at 3.13.2
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-13 20:11:36 -08:00
Ashwin Naren
a5364973d9 implement nt._supports_virtual_terminal
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-13 20:11:05 -08:00
Ashwin Naren
a46ce8ec3a Mark version 3.13.0 (#5495)
* bump to 3.13.1
* fix some tests
* strip left whitespace from doc
* remove specific difflib test that was causing issues
* fix test_enum

Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-13 14:11:01 +09:00
Ashwin Naren
6e35e20e49 dependency bump
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-02-12 18:16:45 +09:00
Noa
2d5e4d89b0 Update openssl to fix possible vulnerability 2025-02-12 15:57:41 +09:00
Hanif Ariffin
c9e62002ec Fixed the implementation of some math functions to match CPython closer. (#5510)
Signed-off-by: Hanif Ariffin <hanif.ariffin.4326@gmail.com>
2025-02-11 17:09:38 +09:00
Lee Dogeon
465627f104 Implement vm logics related with ParamSpec, TypeVarTuple 2025-02-10 21:21:38 +09:00
Ashwin Naren
3de1c2ab56 Update malachite-q and base to 0.4.22 (#5499)
* update malachite-q and base to 0.4.22

* Update malachite-bigint from parser

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2025-02-10 15:55:08 +09:00
Jeong, YunWon
8f5cc6174c fix windows sleep 2025-02-07 07:53:28 +09:00
Jeong YunWon
29d014a0e1 Pin malachite versions to avoid API incompatibility 2025-02-03 11:57:30 +09:00
Ashwin Naren
396a0ca563 Basic Match statements (#5485)
Signed-off-by: Ashwin Naren <arihant2math@gmail.com>
2025-01-25 23:14:15 +09:00
Jeong YunWon
a500178b3c update parser to fix match crash 2025-01-22 13:41:01 +09:00
Jeong YunWon
7d770f55fb more assertions in switch_to_block 2025-01-21 23:53:23 +09:00
Jeong, YunWon
db283a66e8 Merge pull request #5477 from youknowone/better-downcast-error
Add better panic for abnormal downcast error
2025-01-21 13:54:45 +09:00
Jeong, YunWon
c642aef8ca Merge pull request #5481 from arihant2math/builtins-312
Update tests for builtin objects to python 3.12.8
2025-01-20 14:04:28 +09:00
Ashwin Naren
396df1a506 update test_listcomps.py to 3.12.8 2025-01-19 20:05:35 -08:00
Ashwin Naren
9c9fa7e537 update test_list to 3.12.8 2025-01-19 20:05:35 -08:00
Ashwin Naren
86e2eb0648 update test_float to 3.12.8 2025-01-19 20:05:34 -08:00
Ashwin Naren
491db2f0c6 update test_unary to 3.12.8 2025-01-17 18:04:25 -08:00
Ashwin Naren
f0fb375028 update numbers tests 2025-01-17 15:48:59 -08:00
Ashwin Naren
16d8bab61a Update Logging to 3.12.7 (#5478)
* updated logging to 3.12, added logging tests, and added smtplib and tests

* fix expected failures on test_smtplib.py

* mark all rustpython fails on test_logging.py
2025-01-17 19:44:06 +09:00
Ashwin Naren
2d83a67bd6 Update zlib from 3.12.6 and _ZlibDecompressor implementation (#5476)
* add is_s390x and skip_on_s390x to test support

* update zlib tests to 3.12

* _ZlibDecompressor implementation
2025-01-16 13:28:09 +09:00
Jeong YunWon
5ad7e97e05 Add better panic for abnormal downcast error 2025-01-16 00:57:09 +09:00
Jeong YunWon
b7a7b6b923 remove warnings from wasm build 2025-01-13 15:06:29 +09:00
Jeong YunWon
0e00d2328d wasm32-wasi -> wasm32-wasip1 2025-01-13 15:06:29 +09:00
Shubham Patil
53db70e784 Support recursion in JIT-ed functions (#5473) 2025-01-13 14:55:27 +09:00
Sacha Dupuydauby
76c699b4ba Update contextlib from CPython 3.12 2025-01-12 00:40:41 +09:00
Noa
c901bc07a4 Upgrade wasm deps + fix demo 2025-01-11 18:48:27 +09:00
Noa
b7db23bbae Fix warnings for Rust 1.84 2025-01-11 18:48:27 +09:00
Jeong, YunWon
389b20d977 Merge pull request #5444 from key262yek/update_fstring_from_v3.12.7
Update fstring from v3.12.7
2025-01-10 10:44:31 +09:00
Bob McWhirter
d06459fa49 guard signal-handling init more broadly
If `install_signal_handlers` is false (due to embedding),
the VM still inits the signal stdlib and installs a lot
of signal-handling, including touching *ever* signal,
including SIGINT.

When running multiple concurrent interpreters with
varying inits at varying times, this can break the
hosting application's signal-handling so lovingly
set up before starting anything with RustPython.
2025-01-09 16:21:25 -06:00
Shubham Patil
e2a55cbf34 Handle pre-release flag being empty for schedule triggers in release workflow
GitHub workflow_dispatch input variables are always empty for other triggers
2025-01-09 17:31:02 +09:00
Jeong, YunWon
a5e6ade9cb Merge pull request #5454 from coolreader18/rust-1.83
Bump MSRV to 1.83
2025-01-07 13:13:42 +09:00
Jeong, YunWon
a1e32566d3 Merge pull request #5469 from fu050409/patch-1 2025-01-07 12:38:08 +09:00
Noa
8c7bfb3e1a Fix redox 2025-01-06 13:09:49 -06:00
苏向夜
bb0480e978 docs(readme): fix installation command for cargo 2025-01-07 00:52:16 +08:00
Jeong, YunWon
2ccc745513 Merge pull request #5465 from crazymerlyn/caseless-bump 2025-01-04 11:43:30 +09:00
Jeong, YunWon
bea83fe94d Merge pull request #5466 from theshubhamp/gh-release 2025-01-04 11:43:10 +09:00
Shubham Patil
3feaf689d8 Re-enable Release Notes Generation 2025-01-03 21:49:17 +05:30
Shubham Patil
bd627b58af Schedule Pre-Release on Monday 9AM UTC 2025-01-03 21:49:17 +05:30
Shubham Patil
c561d33cb2 Support both Release & Pre-Release in Release Workflow
On Push "main" is removed
2025-01-03 21:49:17 +05:30
Ashwin Naren
c8fd3bd683 Build wasm on release 2025-01-03 14:56:58 +09:00
Ankit Goel
fef1e31634 Bump rust-caseless to 0.2.2 2024-12-31 12:26:29 +00:00
Shubham Patil
1abaf87abe Temporarily disable release notes generation
Release creation fails with this error:
```
HTTP 422: Validation Failed (https://api.github.com/repos/RustPython/RustPython/releases)
body is too long (maximum is 125000 characters)
Error: Process completed with exit code 1.
```

Most likely because there's no previous release to diff of from.

Disabling, getting a green release & enabling back might fix this.
2024-12-30 16:44:55 +09:00
Shubham Patil
38593fbd85 Check operand types in bool or, and, xor to be PyInt (#5461)
* Added Tests for Bitwise or, and, xor type error

* Sync binary operator order comment with actual implementation

* Check operand types in bool or, and, xor to be PyInt

PyNumber methods are expected to type check both arguments.

Dispatch is not done by inverting parameter order for __r<op>__ (example __ror__) when calls are handled via PyNumberMethods
2024-12-30 16:44:27 +09:00
Ankit Goel
8d187fd275 Bump result-like to 0.5.0 2024-12-28 11:38:00 +09:00
Shubham Patil
646cc81656 Add GitHub Binary Release Pipeline for RustPython (#5456) 2024-12-28 11:34:53 +09:00
carsonzhu
01f7536b36 expose run_shell 2024-12-11 17:33:36 +09:00
Jeong, YunWon
97e5ec02f8 Merge pull request #5449 from key262yek/update_test_float_from_CPython_v3.12.7
Update test float from c python v3.12.7
2024-12-06 12:50:55 +09:00
Oskar Skog
3dced01af0 Move os.system from posix.rs to os.rs
Fixes #5100
2024-12-06 12:19:34 +09:00
0cf4534c5c copy new file "Lib/test/support/testcase.py" for test_float.py
Some checks failed
CI / Run rust tests (macos-latest) (pull_request) Has been cancelled
CI / Run rust tests (windows-latest) (pull_request) Has been cancelled
CI / Ensure compilation on various targets (pull_request) Has been cancelled
CI / Run snippets and cpython tests (macos-latest) (pull_request) Has been cancelled
CI / Run snippets and cpython tests (ubuntu-latest) (pull_request) Has been cancelled
CI / Run snippets and cpython tests (windows-latest) (pull_request) Has been cancelled
CI / Check Rust code with rustfmt and clippy (pull_request) Has been cancelled
CI / Run tests under miri (pull_request) Has been cancelled
CI / Check the WASM package and demo (pull_request) Has been cancelled
CI / Run snippets and cpython tests on wasm-wasi (pull_request) Has been cancelled
CI / Run rust tests (ubuntu-latest) (pull_request) Has been cancelled
2024-12-05 15:29:35 +09:00
044f66fba3 copy from cpython v3.12.7 2024-12-05 15:29:35 +09:00
40a9ddad4e update test_fstring.py from cpython 3.12.7
add expectedFailure to tag what should rustpython do
add comment for some syntaxerror which make test run broken
2024-12-05 15:04:56 +09:00
Noa
848db340da Merge pull request #5442 from coolreader18/cli-fixes
Miscellaneous cli-related parity fixes
2024-12-03 23:57:57 -06:00
Noa
c6da4ffcdd Try to fix universal write on windows 2024-12-03 18:01:44 -06:00
Noa
8ac7e34be2 Updates for Rust 1.83 2024-12-03 17:05:24 -06:00
Noa
e4be882994 Miscellaneous cli-related parity fixes 2024-12-03 16:28:32 -06:00
Noa
adc05e663f Merge pull request #5443 from coolreader18/incremental-newline-decoder
Implement IncrementalNewlineDecoder in rust
2024-12-03 16:28:00 -06:00
Jeong, YunWon
0bc236ac98 Merge pull request #5450 from key262yek/resolve_lint_fails_w_rust_1.83_clippy
Resolve lint fails w rust 1.83 clippy
2024-12-02 12:42:37 +09:00
da3d3d9296 allow manual c string literal 2024-11-29 11:57:42 +09:00
6a6af6a952 change to use c str literal 2024-11-29 11:19:10 +09:00
582a4ab267 delete elided lifetime 2024-11-29 11:18:53 +09:00
ebde8546c9 remove elided lifetime 2024-11-29 11:05:44 +09:00
fffd0f5465 new clippy lint update Rust v1.83.0 2024-11-29 09:53:54 +09:00
dependabot[bot]
3b6db8e21a Bump the github-actions group with 7 updates
Bumps the github-actions group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `3` | `4` |
| [actions/setup-python](https://github.com/actions/setup-python) | `4` | `5` |
| [actions/setup-node](https://github.com/actions/setup-node) | `3` | `4` |
| [mwilliamson/setup-wabt-action](https://github.com/mwilliamson/setup-wabt-action) | `1` | `3` |
| [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) | `2` | `4` |
| [wasmerio/setup-wasmer](https://github.com/wasmerio/setup-wasmer) | `2` | `3` |
| [codecov/codecov-action](https://github.com/codecov/codecov-action) | `3` | `5` |


Updates `actions/checkout` from 3 to 4
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

Updates `actions/setup-python` from 4 to 5
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

Updates `actions/setup-node` from 3 to 4
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

Updates `mwilliamson/setup-wabt-action` from 1 to 3
- [Release notes](https://github.com/mwilliamson/setup-wabt-action/releases)
- [Commits](https://github.com/mwilliamson/setup-wabt-action/compare/v1...v3)

Updates `peaceiris/actions-gh-pages` from 2 to 4
- [Release notes](https://github.com/peaceiris/actions-gh-pages/releases)
- [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v2...v4)

Updates `wasmerio/setup-wasmer` from 2 to 3
- [Release notes](https://github.com/wasmerio/setup-wasmer/releases)
- [Commits](https://github.com/wasmerio/setup-wasmer/compare/v2...v3)

Updates `codecov/codecov-action` from 3 to 5
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: mwilliamson/setup-wabt-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: peaceiris/actions-gh-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: wasmerio/setup-wasmer
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-27 17:44:35 -07:00
Noa
19a0b7dd76 Merge pull request #5445 from cclauss/patch-2
Keep GitHub Actions up to date with GitHub's Dependabot
2024-11-27 13:21:11 -06:00
Noa
9647ebe206 Fix prettier formatting 2024-11-27 13:20:19 -06:00
Noa
0c726a1275 Merge pull request #5447 from isbm/isbm-move-exception-loglevel
Move log message to the debug level
2024-11-27 13:04:03 -06:00
Jeong, YunWon
f71eb55f1f Merge pull request #5438 from key262yek/update_random_from_v3.12.3
Update random from v3.12.7
2024-11-26 16:33:36 -07:00
Jeong, YunWon
edcc0b7dbc Merge pull request #5437 from key262yek/update_numbers_from_v3.12.3
Update numbers.py from v3.12.7
2024-11-26 16:30:37 -07:00
Buciu Theodor Marian
4910b308ee Fix compile error when using ``vm-tracing-logging`` 2024-11-26 16:26:00 -07:00
Bo Maryniuk
0cc1c323ed Move log message to the debug level 2024-11-26 21:29:08 +01:00
Christian Clauss
24fd19b35d Keep GitHub Actions up to date with GitHub's Dependabot
Fixes software supply chain safety warnings like at the bottom right of

https://github.com/RustPython/RustPython/actions/runs/11870777239
* [Keeping your actions up to date with Dependabot](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot)
* [Configuration options for the dependabot.yml file - package-ecosystem](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem)
2024-11-26 16:38:54 +01:00
Noa
fbd0c7a99e Implement IncrementalNewlineDecoder in rust 2024-11-14 23:06:53 -06:00
4f5b26698c skip test 'super' object has no attribute 'getstate' 2024-11-11 00:17:57 +09:00
d887e5c24c first copy from cpython v3.12.3 2024-11-11 00:17:30 +09:00
0c69391bbd Copy test for numbers.py from v3.12.3 2024-11-10 00:15:21 +09:00
91598f9121 change numbers.py from [v3.12.3](6293d00e72/Lib/numbers.py (L8)) 2024-11-10 00:13:13 +09:00
Jeong YunWon
98d09e7816 Remove time.daylight from freebsd 2024-10-31 16:13:52 +09:00
7ae9432524 Change deprecated type PanicInfo to PanicHookInfo 2024-10-31 01:45:59 +09:00
Noa
c883f0ad8a Updates for Rust 1.82 2024-10-17 16:32:47 -05:00
Noa
eae60113af Update some stuff for inline const & associated type bounds 2024-10-17 16:32:17 -05:00
Noa
1aab5240cf Update for rust 1.77 2024-10-17 16:32:17 -05:00
dependabot[bot]
edb7abde5a Bump pyo3 from 0.22.3 to 0.22.4
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.3 to 0.22.4.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.3...v0.22.4)

---
updated-dependencies:
- dependency-name: pyo3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-16 05:37:46 +09:00
Noa
29d95340b0 Merge pull request #5418 from crazymerlyn/memoryerror
Handle MemoryError for sequences
2024-09-29 10:50:01 -05:00
Ankit Goel
6fb19ac74f Handle MemoryError for sequences 2024-09-29 16:24:30 +01:00
Noa
37dc28a69d Update deps (#5417)
* Upgrade nix

* Update deps

* Upgrade pyo3, winreg

* Fix errors from upgrading
2024-09-27 13:11:00 +09:00
Ankit Goel
7623668256 Raise TypeError if BaseException receives Keyword arguments 2024-09-24 11:57:37 +09:00
Noa
bbf7aacd4d Merge pull request #5409 from crazymerlyn/cache-frozenset-hash
Cache hash value for FrozenSets
2024-09-23 17:43:55 -05:00
Noa
d6c1e08ef4 Merge pull request #5413 from crazymerlyn/remove-proc-macro-crate
Remove dependency on proc-macro-crate
2024-09-23 17:42:40 -05:00
Ankit Goel
d1f95f04a7 Remove dependency on proc-macro-crate
This dependency is only useful when modifying the crate name of
`num_enum` crate. RustPython doesn't do that so it's unnecessary.
2024-09-23 16:54:21 +01:00
Ankit Goel
0dacf8a326 Remove dependency on uuid-macro-internal
`uuid-macro-internal` is only relevant when using `uuid!` macro to parse
uuids at compile time. RustPython doesn't do that so it's unnecessary.
2024-09-23 15:07:57 +09:00
Noa
e534b10722 Fix build with ossl1 2024-09-22 17:38:26 +09:00
Jeong, YunWon
0785cc5aa9 Merge pull request #5406 from coolreader18/improve-posixsubprocess
Improve posixsubprocess
2024-09-22 17:37:34 +09:00
Ankit Goel
48025e0102 Cache hash value for FrozenSets
Adds a hash field to the `PyFrozenSet` data type in order to avoid
recomputing the hash of an immutable object.
2024-09-22 06:15:54 +01:00
Noa
eeb719e8f7 Merge pull request #5411 from crazymerlyn/decimal-from-float
Fix inconsistent behavior of Decimal.from_float
2024-09-21 22:58:23 -05:00
Noa
7933edad43 Add missing functionality to posixsubprocess 2024-09-21 22:40:15 -05:00
Ankit Goel
5f75728ede Fix inconsistent behavior of Decimal.from_float 2024-09-22 01:42:28 +01:00
Ankit Goel
8cff0ed6c2 Avoid allocating a vector of elements when hashing frozenset (#5408)
Adds a `try_fold_keys` method to Dict which allows performing common
operations on all elements without needing to create a Vec first.
2024-09-21 23:18:26 +09:00
Noa
a8964f4108 Add select.epoll 2024-09-20 11:46:01 +09:00
Noa
740aeedca3 Merge pull request #5405 from crazymerlyn/fix-set-intersection-update
Fix set intersection_update implementation
2024-09-19 21:21:16 -05:00
Noa
8152e7e62c Make Gid/Uid less janky 2024-09-19 17:50:52 -05:00
Ankit Goel
b36c95b91e Fix set intersection_update implementation 2024-09-19 20:59:12 +01:00
Ricardo Robles
b5c1fd95dc Add zoneinfo from Python 3.12.6 (#5400) 2024-09-19 11:18:48 +09:00
Noa
23f7cbf1c3 Make sqlite optional 2024-09-19 11:13:51 +09:00
hongmengning
ae78ecc2c5 Add missing symbols in exceptions.rs 2024-09-17 16:07:30 +09:00
Ankit Goel
dd06516d1c Deprecate delegating int() to __trunc__ 2024-09-17 16:06:09 +09:00
Ankit Goel
8066f36a4e Fix copysign behavior for NaNs (#5396) 2024-09-11 15:19:21 +09:00
Jeong YunWon
3bbf6b9d53 less strict time bound for test_re.test_search_anchor_at_beginning 2024-09-10 16:31:43 +09:00
dependabot[bot]
8bc5944178 Bump webpack from 4.28.2 to 5.94.0 in /wasm/example
Bumps [webpack](https://github.com/webpack/webpack) from 4.28.2 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.28.2...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 14:14:51 +09:00
Niels Buwen
a13b99642b Add get/set methods for function module/annotations (#5392)
---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2024-08-19 00:59:51 +09:00
Jeong, YunWon
060db5983c Merge pull request #5390 from youknowone/wasmbind
recreating wasmbind patch
2024-08-09 09:37:49 +09:00
Jeong YunWon
42bba6920e apply suggestion 2024-08-09 02:23:49 +09:00
Noa
ea11d78995 Fix compilation without compiler feature 2024-08-09 02:23:49 +09:00
Jeong YunWon
fe63ca762f separate wasm-unknown-test 2024-08-09 02:23:49 +09:00
Jeong YunWon
a82982725e fix getrandom 2024-08-09 02:23:49 +09:00
Noa
7dfb760421 Make rustpython-vm compatible with non-js wasm32-unknown & add tests 2024-08-09 02:04:25 +09:00
Jeong YunWon
b6e9a3f37e rustpython_wasm uses the new feature 2024-08-09 00:59:29 +09:00
Jeong YunWon
3f28309b7b revert unnecessary change 2024-08-09 00:58:53 +09:00
Jeong YunWon
d2a4a330f9 following chrono/wasmbind convention 2024-08-09 00:58:11 +09:00
Benjamin DeMann
d8c35770ab enable js feature of getrandom only for wasmbind 2024-08-09 00:29:47 +09:00
Benjamin DeMann
dbb6794a41 add cfg for not wasmbind for time 2024-08-09 00:29:25 +09:00
Benjamin DeMann
63c9909aa0 run the first block if wasmbind is not present 2024-08-09 00:29:25 +09:00
Benjamin DeMann
f1dac5087e address pr notes 2024-08-09 00:29:25 +09:00
Benjamin DeMann
4f80d7013e add wasmbind feature 2024-08-09 00:29:25 +09:00
Jeong YunWon
2919df1df5 Mark rust-version 1.78 2024-08-08 23:20:17 +09:00
Jeong YunWon
a2df2f014b Fix dis.dis without compiler feature 2024-08-07 09:48:09 +09:00
Jeong YunWon
8673169ee7 0.4.0 2024-08-07 08:38:21 +09:00
Jeong YunWon
2c2e0fb172 pyperf bench script 2024-08-07 08:38:21 +09:00
Jeong YunWon
d45c5de906 ignore local config files 2024-08-07 08:38:21 +09:00
Jeong YunWon
eb985beed6 freeze-stdlib imply stdlib 2024-08-07 07:48:42 +09:00
Jeong YunWon
ff9aa0fc54 Add example projects 2024-08-07 07:48:42 +09:00
Jeong YunWon
0bd1a3efb2 vm.print 2024-08-07 07:06:27 +09:00
Jeong YunWon
08e7ec948b Don't let Interpreter::run silently ignore result value 2024-08-07 07:06:15 +09:00
Jeong YunWon
6092c07eb5 better error message for freeze-stdlib 2024-08-07 07:05:28 +09:00
CPython Developers
09d74336fa Update fraction,test_numeric_tower from CPython 3.12.4 2024-07-29 19:53:44 +09:00
Jeong YunWon
694354e5e6 temp fix slot member_count 2024-07-29 16:45:35 +09:00
Andrew Bowen
55f04db6b8 Update codecs and test_codecs from cpython v3.12.4 (#5372)
* updated test_codecs.py from cpython v3.12.4

* updated codecs.py from cpython v3.12.4
2024-07-29 14:55:32 +09:00
Jeong YunWon
6f8360a878 sys.exception 2024-07-29 14:30:59 +09:00
Jeong YunWon
6ca4685fee __objclass__ 2024-07-29 14:30:46 +09:00
Jeong, YunWon
d86b592add Merge pull request #5370 from youknowone/update-io
Update io, _pyio, test_io from CPython 3.12
2024-07-28 21:12:13 +09:00
Jeong YunWon
9944b3dac1 Mark failing tests 2024-07-28 20:24:15 +09:00
Jeong YunWon
8ed79bd1af add a dummy parameter for support.skip_if_sanitizer 2024-07-28 20:24:15 +09:00
CPython Developers
0600ae6213 Update io, pyio, test_io from CPython 2024-07-28 20:24:15 +09:00
Jeong YunWon
623415d843 IncrementalNewlineDecoder from pyio 2024-07-28 20:23:53 +09:00
Jeong YunWon
87b84a83cc Fix TextIOWrapper.reconfigure coder 2024-07-28 20:23:53 +09:00
Jeong YunWon
aa5eba9723 fix BufferedMixin::tell 2024-07-28 20:23:53 +09:00
Jeong YunWon
c2cd883f93 TextIOWrapper.reconfigure 2024-07-28 16:24:32 +09:00
Jeong YunWon
43e20a8cd9 Fix os.putenv & test_os.test_envronb 2024-07-28 16:24:17 +09:00
Jeong YunWon
48299cdd7f Add faulthandler to wasm32 2024-07-28 12:11:56 +09:00
Dan Näsman
2335ca0f72 Add proper underscore handling to float and complex types. (#5356) 2024-07-28 12:11:34 +09:00
Jeong YunWon
e42c1a33b5 upgrade unicode_names2 2024-07-28 11:43:00 +09:00
Jeong, YunWon
b1a38c2d50 Fix rust 1.80 warnings (#5363)
* Fix rust 1.80 warnings

* Fix nightly clippy warnings
2024-07-28 10:43:40 +09:00
dependabot[bot]
f1f05303db Bump openssl from 0.10.62 to 0.10.66
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.62 to 0.10.66.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.62...openssl-v0.10.66)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-23 11:14:45 +09:00
Jeong YunWon
8424a733a2 type constructor 2024-07-14 22:08:09 +09:00
Jeong YunWon
2bf7a4a08c FuncArgs::is_empty() 2024-07-14 22:08:09 +09:00
Jeong, YunWon
b4bae8173b Merge pull request #5348 from JamesClarke7283/developer-experiance-tweaks
DX improvements: Updated MSRV, added cargo deny toml file, and added lints to workspace
2024-07-01 21:09:49 +09:00
James Clarke
3c10888e3a Disabled some uneeded lits 2024-06-30 13:41:40 +01:00
James Clarke
1bd143027a Switched to system implementation of libffi and fixed minor lint errors 2024-06-30 13:29:04 +01:00
James Clarke
18d31f2d5b Merge branch 'RustPython:main' into developer-experiance-tweaks 2024-06-30 12:34:12 +01:00
James Clarke
e142d655b9 changed int_max_str_digits from -1 to 4300 to be more cpython complient (#5343)
* changed int_max_str_digits from -1 to 4300 to be more cpython complient, fixes #5139
2024-06-27 15:27:54 +09:00
James Clarke
e1ce1f66cd Updated MSRV 2024-06-25 04:56:22 +01:00
James Clarke
08c9a4d86b Added lints and deny.toml 2024-06-25 03:59:40 +01:00
Kirill Podoprigora
a620b38ba0 Add test_copyreg.py from 3.12 2024-06-24 19:00:54 +09:00
Jeong, YunWon
8e3c0cd658 Merge pull request #5314 from youknowone/datetime
Update datetime from CPython v3.12.3
2024-06-22 17:38:59 +09:00
Jeong YunWon
f709a2805d mark failing tests 2024-06-22 16:18:34 +09:00
Jeong, YunWon
adbadfc4f5 Merge pull request #5342 from youknowone/getstate
object.__getstate__
2024-06-22 15:49:23 +09:00
CPython Developers
9c7a9cbace Update datetime from CPython v3.12.3 2024-06-22 15:27:19 +09:00
CPython Developers
1333688a4e update copyreg from CPython 3.12.3 2024-06-22 15:24:42 +09:00
Jeong YunWon
17852b78d5 mark hanging test 2024-06-22 15:24:42 +09:00
Jeong YunWon
dd15ae5560 Unmark successful tests 2024-06-22 15:24:42 +09:00
Jeong YunWon
9d33990199 object.__getstate__ 2024-06-22 15:15:49 +09:00
Cherry
866e7cf371 Make a shared run function in ThreadedVirtualMachine (#5337) 2024-06-22 14:43:07 +09:00
Jeong YunWon
f5c1596ddf PyCFunction_GET_SELF 2024-06-22 00:40:30 +09:00
Jeong YunWon
64cff172a1 pymethod(raw) 2024-06-22 00:40:13 +09:00
kingler
c10b99b29a Update keyword.py and test_keyword.py from CPython v3.12.0 2024-06-21 22:37:15 +09:00
Snowapril
bca90f191c Fix missing_transmute_annotations, legacy_numeric_constants added in Rust 1.79.0 clippy (#5339) 2024-06-16 20:26:22 +09:00
Jonathan Rotter
7996a10116 await in list comprehension (#5334)
* check if comprehension element contains await

* force execution to pause in async gen
2024-05-27 16:54:56 +09:00
Dmitry Mottl
db4562f67d Fixes a typo 2024-05-22 10:07:44 +09:00
Noa
3fd0382a51 Use RPITIT where applicable 2024-05-21 14:47:10 +09:00
Noa
3e98e5e86c Bump libsqlite3-sys 2024-05-21 09:05:49 +09:00
Jeong YunWon
7b17965b26 Enable macOS SSL 2024-05-21 09:05:26 +09:00
Jeong YunWon
5c050d5779 feature ssl-vendor implies ssl 2024-05-20 19:41:51 +09:00
Jeong, YunWon
5d6d1a6da7 OpenSSL3 in README 2024-05-20 13:07:36 +09:00
Daniel Chiquito
b59d876e76 Make __bases__ mutable (#5325)
* Make PyType.bases and PyType.mro mutable

* Add a set function for __bases__

* Re-enable fixed test_descr tests
2024-05-20 12:28:07 +09:00
Jeong YunWon
9028aede6e Reduce rc ops from best_base 2024-05-19 19:24:52 +09:00
Snowapril
aa0353a501 Fix best_base to select proper base class (#5324)
* add __basicsize__ getter

* modify best_base function to get proper base

* inherit basicsize from the base type

---------

Signed-off-by: snowapril <sinjihng@gmail.com>
2024-05-19 15:48:56 +09:00
Jeong YunWon
63c3f31c99 skip flaky test in test_context 2024-05-17 14:02:40 +09:00
Jeong YunWon
2fe44af8ba time.{tzname,timezone,daylight} 2024-05-14 11:17:58 +09:00
matheusfgoncalves95
515f0bf9c2 Adding generator_stop to compile_future_features (#5320)
* Adding generator_stop to compile_future_features.

* Removing expected failure from PEP 479 unit test.
2024-05-13 23:18:55 +09:00
hydrogen602
27a52a7962 test_class.py: testTypeAttributeAccessErrorMessages fix 2024-05-13 16:25:37 +09:00
hydrogen602
61feb43aba test_builtin.py: test_type_qualname fix 2024-05-13 14:18:18 +09:00
Jonathan Rotter
07fdcb6160 Fix for test future.py test annotations (#5319)
* enable test fixed in parser

* update parser dependency
2024-05-13 08:40:14 +09:00
Daniel Chiquito
ac08f4447f Add 3.12 typing features to the compiler (#5302) 2024-05-10 20:09:04 +09:00
Jeong, YunWon
d243c90d0a Update site from CPython v3.12.3 (#5313)
* Update site from CPython v3.12.3

* mark failing test

---------

Co-authored-by: CPython Developers <>
2024-05-09 23:04:31 +09:00
Jeong, YunWon
72033ab689 Update ipaddress from CPython v3.12.3 (#5312)
Co-authored-by: CPython Developers <>
2024-05-09 15:56:11 +09:00
Jeong, YunWon
3f63501fa8 Merge pull request #5311 from youknowone/pathlib
Update pathlib from CPython v3.12.3
2024-05-09 15:46:25 +09:00
Jeong YunWon
019496e3a8 Instruction::ReturnConst 2024-05-09 15:26:16 +09:00
Jeong YunWon
52695a9ece Skip tests to avoid slot bug 2024-05-09 15:20:38 +09:00
CPython Developers
a20ce951e7 Update pathlib from CPython v3.12.3 2024-05-09 15:20:25 +09:00
Jeong YunWon
85fa157d5a Update test_os from CPython v3.12.3 2024-05-09 11:51:05 +09:00
Jeong, YunWon
1068219c56 Update Lib/asyncio from CPython v3.12.3 (#3858)
Co-authored-by: CPython Developers <>
2024-05-09 05:02:46 +09:00
Jeong, YunWon
427ec50624 _overlapped 2024-05-09 04:41:39 +09:00
Jeong, YunWon
52ce1509a5 optimize range.__contains__ and iter_search 2024-05-09 01:26:09 +09:00
Jeong YunWon
fe06583643 posix.sysconf 2024-05-08 18:46:17 +09:00
Jeong YunWon
3e3c69b5bc Fix non-msvc build 2024-05-08 16:30:41 +09:00
Jeong YunWon
3b0802535d _winapi.NULL 2024-05-08 16:30:41 +09:00
Jeong YunWon
75415090bd ConfName -> PathConfName 2024-05-08 14:50:51 +09:00
Jeong YunWon
6f664cb05a Fix initgroups nul error 2024-05-08 14:50:51 +09:00
Jonathan Rotter
a8ea67178d adjusted SyntaxError args and __str__() to match CPython (#5294) 2024-05-08 12:57:03 +09:00
Jeong, YunWon
64a464aefb Update audiotests from CPython 3.12.3 (#5304)
Co-authored-by: moonlightaria <edch66@gmail.com>
2024-05-08 12:52:33 +09:00
jparag
67885ad9fa Update gettext.py and related test to 3.12 version (#5287)
Co-authored-by: Parag Jain <parag.jain2@capitalone.com>
2024-05-08 00:02:59 +09:00
Jeong YunWon
e5bf72e897 contextvars 2024-05-07 22:43:21 +09:00
Jeong YunWon
1f62190eef Use Sequence protocol for in operation 2024-05-07 21:33:14 +09:00
Jeong YunWon
f839e6cc79 more #[pyclass(Py, PyRef)] 2024-05-06 01:32:46 +09:00
Jeong YunWon
52aad1ec08 DefaultConstructor impls Constructor; Unconstructible is not anymore 2024-05-06 00:54:39 +09:00
Jeong YunWon
5f0d1252b6 suppress empty-doc warning 2024-05-04 02:00:18 +09:00
Jeong YunWon
3370f0f23d Update wasm-bindgen 2024-05-04 02:00:18 +09:00
Jeong YunWon
e1574b1485 sys.{get,set}_asyncgen_hooks 2024-05-02 13:45:03 +09:00
Jeong YunWon
78fae736f9 sys.get_coroutine_origin_tracking_depth 2024-05-01 21:32:14 +09:00
Jeong YunWon
ed3811a65c wasi select module 2024-05-01 17:54:24 +09:00
Jeong, YunWon
4e915de35d Merge pull request #5290 from youknowone/subprocess
Update subprocess from CPython 3.12.3
2024-05-01 16:24:35 +09:00
CPython Developers
94e6648ee5 Update selectors from CPython v3.12.3 2024-05-01 15:59:23 +09:00
Jeong YunWon
75a985e63e mark failng tests 2024-05-01 15:59:23 +09:00
CPython Developers
5714558785 Update subprocess from CPython 3.12.3 2024-05-01 04:23:14 +09:00
Jeong, YunWon
c3ccb5b7bf Merge pull request #5289 from youknowone/fixes
Align Settings to CPython PyConfig
2024-04-30 23:18:43 +09:00
Jeong YunWon
87849cda4f fix grammar 2024-04-30 23:18:14 +09:00
Jeong YunWon
f62114c7eb zlib-ng as default 2024-04-30 23:18:14 +09:00
Jeong YunWon
24f57dde2f Align Settings to PyConfig 2024-04-30 23:18:14 +09:00
Jeong, YunWon
cdfcd8a4c9 Merge pull request #5288 from youknowone/enable-wasi-unittest
Enable wasi unittest
2024-04-30 23:13:07 +09:00
Jeong YunWon
1bc1370701 Add wasi unittest CI 2024-04-30 22:21:56 +09:00
Jeong YunWon
6b7b080827 Add sys._stdlib_dir when freeze-stdlib 2024-04-30 22:21:56 +09:00
Jeong YunWon
3556e1320d wasm os.getpid 2024-04-29 14:42:02 +09:00
Jeong YunWon
621640ca71 Add const-only signals for wasm32 2024-04-28 10:31:38 +09:00
Jeong YunWon
2c0e439d0d Disable jit for macos from CI to avoid arm64 CI failure 2024-04-28 00:07:55 +09:00
Jeong YunWon
a392d8425e Fix wasmer example 2024-04-26 20:57:32 +09:00
Jeong YunWon
0273d78de9 Update signal from CPython 3.12.3 2024-04-26 01:39:02 +09:00
Jeong YunWon
ed51d8dcf6 upgrade geckodriver 2024-04-25 23:11:56 +09:00
Jeong, YunWon
3d78ca8b09 Merge pull request #5263 from youknowone/multiprocessing
Update multiprocessing from CPython 3.12.3
2024-04-25 22:05:11 +09:00
Jeong YunWon
1cec856c63 skip flaky test_socket sendmsg tests in macos 2024-04-25 22:04:40 +09:00
Jeong, YunWon
6212c81ec6 Merge pull request #5276 from youknowone/async-for-comprehension
Async for comprehension
2024-04-25 21:00:22 +09:00
Jeong YunWon
408459b883 vm runtime debug prints 2024-04-25 20:34:53 +09:00
Jeong YunWon
f6d88042b5 Uncomment test_grammar tests 2024-04-25 20:23:34 +09:00
Jeong YunWon
b58bdc9e32 Uncomment async for tests 2024-04-25 20:23:34 +09:00
Jeong YunWon
f3501f44cb Basic async for comprehension support 2024-04-25 20:23:25 +09:00
HyeockJinKim
61f37b10e2 implement async for function in compiler 2024-04-25 20:20:58 +09:00
Aaron Long
2856aff757 Begin async list comprehension implementation 2024-04-25 20:20:22 +09:00
Jeong YunWon
3949ecc054 implement more GetANext 2024-04-25 19:16:48 +09:00
Jeong YunWon
acd9ea57d7 Fix async for block 2024-04-25 17:40:15 +09:00
Jeong YunWon
794a2a1892 remove pop-push 2024-04-25 17:40:15 +09:00
hydrogen602
af8bed6d3f filter out doc strings with opt level 2 2024-04-25 16:11:04 +09:00
Jeong YunWon
462f54f906 Fix with __exit__ error handling 2024-04-25 15:17:50 +09:00
Jeong YunWon
1c4e99cf2c popblock debug 2024-04-25 15:17:50 +09:00
Jeong YunWon
a349b9bdfe frame debug codes 2024-04-24 21:46:14 +09:00
Jeong YunWon
75e790836a debuggable 2024-04-24 20:44:17 +09:00
hardlydearly
5ba677cd36 chore: fix some typos in comments
Signed-off-by: hardlydearly <799511800@qq.com>
2024-04-24 17:52:58 +09:00
Jeong, YunWon
a7b761a89c windows symlink flag 2024-04-24 02:43:01 +09:00
Jeong, YunWon
6e0b00d60c Merge pull request #5262 from youknowone/ntpath
Update ntpath from CPython 3.12.3
2024-04-24 01:46:49 +09:00
Jeong YunWon
c107a938be remove -u all from mac/windows 2024-04-24 01:46:27 +09:00
Jeong YunWon
c6fc9cee8e Update filecmp from CPython 3.12.2 2024-04-24 01:46:27 +09:00
Jeong YunWon
99a4bf5eb5 remove import dummp_os 2024-04-24 01:46:27 +09:00
CPython Developers
46410ff933 Add test_multiprocessing from CPython 3.12.3 2024-04-24 01:15:34 +09:00
CPython Developers
75e868e71e Update multiprocessing from CPython 3.12.3 2024-04-24 01:15:08 +09:00
Jeong, YunWon
53b1b4d682 Mark failing tests in test_ntpath 2024-04-24 00:54:12 +09:00
CPython Developers
566d9bee5c Update ntpath from CPython 3.12.3 2024-04-24 00:54:12 +09:00
Jeong, YunWon
b8bf65d68e os.listdrives 2024-04-24 00:54:09 +09:00
Jeong YunWon
075c69a3cd Skip setattr(__doc__) if not given 2024-04-23 22:44:22 +09:00
Jeong, YunWon
ada10067dd Merge pull request #5260 from youknowone/update-pickle
Update pickle from CPython 3.12.3
2024-04-23 21:37:32 +09:00
CPython Developers
d0f680b379 Update pickletools from CPython 3.12.3 2024-04-23 18:05:50 +09:00
Jeong YunWon
ca2c1d0b48 Update pickle from CPython 3.12.2 2024-04-23 16:46:16 +09:00
Jeong YunWon
5fd5939395 skip flaky socket test in macOS 2024-04-23 16:24:15 +09:00
Jeong, YunWon
e5ca631b52 Add bare ExceptionGroup (#5259) 2024-04-23 16:23:31 +09:00
Jonathan Rotter
3313fdeb32 test_builtin.py test_compile unit test fix (#5251)
* compile accepts memoryview now

* Update test_compile to what it is in CPython3.12

* compile optimize flag

* added undocumented flags to flag validator
2024-04-23 14:07:02 +09:00
Jeong, YunWon
2bd8ff0b6c Merge pull request #5257 from youknowone/os-posix
Update os/posix/posixpath from CPython 3.12.2
2024-04-23 13:33:40 +09:00
CPython Developers
5c9d6d455d Update genericpath/posixpath from CPython 3.12.2 2024-04-23 12:56:43 +09:00
CPython Developers
8f6cf6fef7 Update test_posix from CPython 3.12.2 2024-04-23 12:56:43 +09:00
CPython Developers
a5f8d42d34 Update os from CPython 3.12.2 2024-04-23 12:56:32 +09:00
CPython Developers
153ec2823d Update __future__ from CPython 3.12.2 2024-04-23 11:57:46 +09:00
CPython Developers
9f65a30504 Update encodings from CPyhton 3.12.2 2024-04-22 12:46:32 +09:00
CPython Developers
b018123f19 Update getopt from CPython 3.12.2 2024-04-22 12:46:32 +09:00
CPython Developers
97e3e969e4 Update glob from CPython 3.12.2 2024-04-22 12:46:32 +09:00
Noa
84099514e6 Implement socket.socket.sendmsg (#5205)
* Implement socket.socket.sendmsg

* debugger-friendly newlines

* Fix control_buf error on macOS

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
2024-04-22 11:21:10 +09:00
Jeong, YunWon
3286e683e6 win32_xstat (#5247)
* win32_xstat

* vm::windows

* Add file_id
2024-04-21 22:02:09 +09:00
hydrogen602
be6ea2a7b8 fix for test test_bool.BoolTest.test_subclass 2024-04-21 07:09:08 +09:00
Jeong, YunWon
4a939141f6 Fix macos shutil (#5248)
* Fix macos shutil

* chmod+follow_symlinks=False calls lchmod
2024-04-21 07:07:58 +09:00
Jonathan Rotter
1034477f1e unit test fix - only allow catching exceptions - test_catch_non_BaseException (#5249)
* test_baseexception.test_catch_non_BaseException

* enabling test_except_star_invalid_exception_type
2024-04-21 01:30:20 +09:00
Jeong, YunWon
49cfcd817d remove some skipIf and enable shutil CI for windows (#5235)
* mark failing shtuil tests in windows

* unsetenv raise more informed error

* Remove a few skipIf for windows
2024-04-20 03:00:27 +09:00
Jeong, YunWon
39822d7e57 use less vm.new_os_error (#5245)
* vm.new_errno_error

* replace new_os_error in mmap

* replace posix new_os_error to new_errno_error
2024-04-20 01:32:33 +09:00
Jeong YunWon
142d333c5c Fix os.unsetenv 2024-04-20 00:36:00 +09:00
Jeong YunWon
13c491712b add six install to CI 2024-04-19 22:45:25 +09:00
Jeong, YunWon
5c527c2a28 Fix shutil test_copy_dir 2024-04-19 22:04:22 +09:00
toofooboo
41979f0823 chore: fix some typos in comments (#5243)
Signed-off-by: toofooboo <cmaker@foxmail.com>
2024-04-19 21:10:22 +09:00
Jeong, YunWon
192b0a8fb5 fileutils and fstat (#5242) 2024-04-19 19:21:35 +09:00
Jonathan Rotter
6a7be1e142 compat fix for testHashStuff in test_class.py (#5240)
* compat fix for testHashStuff in test_class.py

* accidently fixed 3 more tests
2024-04-19 16:44:23 +09:00
Jeong, YunWon
b3666060d4 better OSError.filename (#5239)
* Fix OSError message

* IOError with filename

* more filenames
2024-04-19 15:53:11 +09:00
Jeong, YunWon
c9c07a6bbc vm/ospath module 2024-04-19 13:25:19 +09:00
Jeong, YunWon
d57287b89b Fix OSError.winerror and errno/winerror converions 2024-04-19 04:56:25 +09:00
Jeong, YunWon
79e7015a32 CreateJunction 2024-04-19 03:00:33 +09:00
Jeong, YunWon
6c4ee951e0 fix windows OSError 2024-04-18 10:23:12 +09:00
Jeong YunWon
f0f4633346 Fix nightly clippy 2024-04-18 03:37:29 +09:00
hydrogen602
6c37e8f4e7 compat fix for test_field_metadata_custom_mapping in test_dataclasses.py 2024-04-17 21:52:10 +09:00
CPython Developers
4d05416ce3 Update ensurepip from CPython 3.12 2024-04-17 12:03:31 +09:00
Lee Dogeon
60993b9d23 Bump shutil to 3.12.3 (#5228)
* Bump shutil to 3.12.3

* Implement windows platform target `is_junction` with junction library

* Add _winapi.NeedCurrentDirectoryForExePath

---------

Co-authored-by: Jeong, YunWon <jeong@youknowone.org>
2024-04-17 01:33:14 +09:00
Moreal
959e7c11ce Unmark fixed tests 2024-04-15 16:22:53 +09:00
Moreal
21ae739eec Check downcast to type first 2024-04-15 16:22:53 +09:00
Moreal
b1c3c9a9d6 Bump CPython version to 3.12.3 in CI 2024-04-14 13:07:13 +09:00
Moreal
bf985c8ac6 Fix None.__ne__ bug 2024-04-14 13:07:13 +09:00
Moreal
21c5eae717 Add testcase for __ne__ method 2024-04-14 13:07:13 +09:00
Lee Dogeon
4d5cf249a0 Allow update __class__ for mutable types or module subclasses (#5225)
* Allow update `__class__` for module subclasses

* Set `IMMUTABLETYPE` flag for static classes

* Unmark fixed tests
2024-04-14 13:03:48 +09:00
Jeong YunWon
8c7b811135 Fix _sha512 support 2024-04-11 21:17:53 +09:00
Jeong, YunWon
940b879950 Merge pull request #5222 from youknowone/vm-build
Fix rustpython-vm --no-default-features build
2024-04-11 15:29:43 +09:00
Jeong YunWon
ccf650d4ca Fix rustpython-vm --no-default-features build 2024-04-10 19:15:22 +09:00
Jeong, YunWon
6342f16eb5 Merge pull request #5219 from youknowone/0.3.1
0.3.1
2024-04-10 17:41:28 +09:00
1156 changed files with 170091 additions and 58378 deletions

3
.coderabbit.yml Normal file
View File

@@ -0,0 +1,3 @@
reviews:
path_filters:
- "!Lib/**"

61
.cspell.dict/cpython.txt Normal file
View File

@@ -0,0 +1,61 @@
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
subparams
tok_oldval
tvars
unaryop
unparse
unparser
VARKEYWORDS
varkwarg
wbits
weakreflist
withitem
withs
xstat
XXPRIME

View File

@@ -0,0 +1,265 @@
abiflags
abstractmethods
aenter
aexit
aiter
anext
appendleft
argcount
arrayiterator
arraytype
asend
asyncgen
athrow
backslashreplace
baserepl
basicsize
bdfl
bigcharset
bignum
bivariant
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
getframemodulename
getnewargs
getpip
getrandom
getrecursionlimit
getrefcount
getsizeof
getswitchinterval
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
PYTHONDONTWRITEBYTECODE
PYTHONHASHSEED
PYTHONHOME
PYTHONINSPECT
PYTHONINTMAXSTRDIGITS
PYTHONNOUSERSITE
PYTHONOPTIMIZE
PYTHONPATH
PYTHONPATH
PYTHONSAFEPATH
PYTHONUNBUFFERED
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
setswitchinterval
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

View File

@@ -0,0 +1,86 @@
ahash
arrayvec
bidi
biguint
bindgen
bitand
bitflags
bitor
bitxor
bstr
byteorder
byteset
caseless
chrono
consts
cranelift
cstring
datelike
deserializer
deserializers
fdiv
flamescope
flate2
fract
getres
hasher
hexf
hexversion
idents
illumos
indexmap
insta
keccak
lalrpop
lexopt
libc
libcall
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

View File

@@ -1,209 +1,85 @@
// 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__/**",
"target/**",
"Lib/**"
],
// words - list of words to be always considered correct
"words": [
// Rust
"ahash",
"bidi",
"biguint",
"bindgen",
"bitflags",
"bstr",
"byteorder",
"chrono",
"consts",
"cstring",
"flate2",
"fract",
"hasher",
"idents",
"indexmap",
"insta",
"keccak",
"lalrpop",
"libc",
"libz",
"longlong",
"Manually",
"maplit",
"memmap",
"metas",
"modpow",
"nanos",
"peekable",
"powc",
"powf",
"prepended",
"punct",
"replacen",
"rsplitn",
"rustc",
"rustfmt",
"seekfrom",
"splitn",
"subsec",
"timsort",
"trai",
"ulonglong",
"unic",
"unistd",
"winapi",
"winsock",
// Python
"abstractmethods",
"aiter",
"anext",
"arrayiterator",
"arraytype",
"asend",
"athrow",
"basicsize",
"cformat",
"classcell",
"closesocket",
"codepoint",
"codepoints",
"cpython",
"decompressor",
"defaultaction",
"descr",
"dictcomp",
"dictitems",
"dictkeys",
"dictview",
"docstring",
"docstrings",
"dunder",
"eventmask",
"fdel",
"fget",
"fileencoding",
"fillchar",
"finallyhandler",
"frombytes",
"fromhex",
"fromunicode",
"fset",
"fspath",
"fstring",
"fstrings",
"genexpr",
"getattro",
"getformat",
"getnewargs",
"getweakrefcount",
"getweakrefs",
"hostnames",
"idiv",
"impls",
"infj",
"instancecheck",
"instanceof",
"isabstractmethod",
"itemiterator",
"itemsize",
"iternext",
"keyiterator",
"kwarg",
"kwargs",
"linearization",
"linearize",
"listcomp",
"mappingproxy",
"maxsplit",
"memoryview",
"memoryviewiterator",
"metaclass",
"metaclasses",
"metatype",
"mro",
"mros",
"nanj",
"ndigits",
"ndim",
"nonbytes",
"origname",
"posixsubprocess",
"pyexpat",
"PYTHONDEBUG",
"PYTHONHOME",
"PYTHONINSPECT",
"PYTHONOPTIMIZE",
"PYTHONPATH",
"PYTHONPATH",
"PYTHONVERBOSE",
"PYTHONWARNINGS",
"qualname",
"radd",
"rdiv",
"rdivmod",
"reconstructor",
"reversevalueiterator",
"rfloordiv",
"rlshift",
"rmod",
"rpow",
"rrshift",
"rsub",
"rtruediv",
"scproxy",
"setattro",
"setcomp",
"showwarnmsg",
"warnmsg",
"stacklevel",
"subclasscheck",
"subclasshook",
"unionable",
"unraisablehook",
"valueiterator",
"vararg",
"varargs",
"varnames",
"warningregistry",
"warnopts",
"weakproxy",
"xopts",
// RustPython
"RUSTPYTHONPATH",
// RustPython terms
"aiterable",
"alnum",
"baseclass",
"boxvec",
"Bytecode",
"cfgs",
"codegen",
"coro",
"dedentations",
"dedents",
"deduped",
"downcastable",
"downcasted",
"dumpable",
"emscripten",
"excs",
"finalizer",
"GetSet",
"groupref",
"internable",
"jitted",
"jitting",
"lossily",
"makeunicodedata",
"miri",
"notrace",
"openat",
"pyarg",
"pyarg",
"pyargs",
"pyast",
"PyAttr",
"pyc",
"PyClass",
@@ -212,6 +88,8 @@
"PyFunction",
"pygetset",
"pyimpl",
"pylib",
"pymath",
"pymember",
"PyMethod",
"PyModule",
@@ -224,6 +102,7 @@
"PyResult",
"pyslot",
"PyStaticMethod",
"pystone",
"pystr",
"pystruct",
"pystructseq",
@@ -231,57 +110,33 @@
"reducelib",
"richcompare",
"RustPython",
"significand",
"struc",
"summands", // plural of summand
"sysmodule",
"tracebacks",
"typealiases",
"Unconstructible",
"unconstructible",
"unhashable",
"uninit",
"unraisable",
"unresizable",
"wasi",
"zelf",
// cpython
"argtypes",
"asdl",
"asname",
"augassign",
"badsyntax",
"basetype",
"boolop",
"bxor",
"cellarg",
"cellvar",
"cellvars",
"cmpop",
"dictoffset",
"elts",
"excepthandler",
"finalbody",
"freevar",
"freevars",
"fromlist",
"heaptype",
"IMMUTABLETYPE",
"kwonlyarg",
"kwonlyargs",
"linearise",
"maxdepth",
"mult",
"nkwargs",
"orelse",
"patma",
"posonlyarg",
"posonlyargs",
"prec",
"stackdepth",
"unaryop",
"unparse",
"unparser",
"VARKEYWORDS",
"varkwarg",
"wbits",
"withitem",
"withs"
// unix
"CLOEXEC",
"codeset",
"endgrent",
"gethrvtime",
"getrusage",
"nanosleep",
"sigaction",
"WRLCK",
// win32
"birthtime",
"IFEXEC",
// "stat"
"FIRMLINK"
],
// flagWords - list of words to be always considered incorrect
"flagWords": [

6
.devcontainer/Dockerfile Normal file
View 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/*

View File

@@ -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"
}

2
.gemini/config.yaml Normal file
View File

@@ -0,0 +1,2 @@
ignore_patterns:
- "Lib/**"

3
.gitattributes vendored
View File

@@ -1,6 +1,7 @@
Lib/** linguist-vendored
Cargo.lock linguist-generated -merge
Cargo.lock linguist-generated
*.snap linguist-generated -merge
vm/src/stdlib/ast/gen.rs linguist-generated -merge
Lib/*.py text working-tree-encoding=UTF-8 eol=LF
**/*.rs text working-tree-encoding=UTF-8 eol=LF
*.pck binary

212
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,212 @@
# 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). **IMPORTANT**: Do not edit this directory directly; The only allowed operation is copying files 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 ${TEST_MODULE}
cargo run --release -- -m test test_unicode # to test test_unicode.py
# Run the Python test module with specific function
cargo run --release -- -m test test_unicode -k test_unicode_escape
```
### 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)
- **IMPORTANT**: Always run clippy to lint code (`cargo clippy`) before completing tasks. Fix any warnings or lints that are introduced by your changes
- 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
- **IMPORTANT**: In most cases, Python code should not be edited. Bug fixes should be made through Rust code modifications only
- 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
```
## Test Code Modification Rules
**CRITICAL: Test code modification restrictions**
- NEVER comment out or delete any test code lines except for removing `@unittest.expectedFailure` decorators and upper TODO comments
- NEVER modify test assertions, test logic, or test data
- When a test cannot pass due to missing language features, keep it as expectedFailure and document the reason
- The only acceptable modifications to test files are:
1. Removing `@unittest.expectedFailure` decorators and the upper TODO comments when tests actually pass
2. Adding `@unittest.expectedFailure` decorators when tests cannot be fixed
**Examples of FORBIDDEN modifications:**
- Commenting out test lines
- Changing test assertions
- Modifying test data or expected results
- Removing test logic
**Correct approach when tests fail due to unsupported syntax:**
- Keep the test as `@unittest.expectedFailure`
- Document that it requires PEP 695 support
- Focus on tests that can be fixed through Rust code changes only
## 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
View 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

View File

@@ -4,6 +4,7 @@ on:
pull_request:
types: [unlabeled, opened, synchronize, reopened]
merge_group:
workflow_dispatch:
name: CI
@@ -15,26 +16,26 @@ concurrency:
cancel-in-progress: true
env:
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,ssl,jit
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_importlib
test_io
test_os
test_rlcompleter
test_pathlib
test_posixpath
test_shutil
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
@@ -59,7 +60,6 @@ env:
test_dis
test_enumerate
test_exception_variations
test_exceptions
test_float
test_format
test_fractions
@@ -100,12 +100,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.12.0"
PYTHON_VERSION: "3.13.1"
jobs:
rust_tests:
@@ -114,12 +113,13 @@ jobs:
RUST_BACKTRACE: full
name: Run rust tests
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ contains(matrix.os, 'windows') && 45 || 30 }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
@@ -128,6 +128,7 @@ jobs:
- name: Set up the Windows environment
shell: bash
run: |
git config --system core.longpaths true
cargo install --target-dir=target -v cargo-vcpkg
cargo vcpkg -v build
if: runner.os == 'Windows'
@@ -136,19 +137,24 @@ jobs:
if: runner.os == 'macOS'
- name: run clippy
run: cargo clippy ${{ env.CARGO_ARGS }} --workspace --exclude rustpython_wasm -- -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 }}
if: runner.os != 'macOS'
# temp skip ssl linking for Mac to avoid CI failure
- name: run rust tests (MacOS no ssl)
run: cargo test --workspace --exclude rustpython_wasm --verbose --no-default-features --features threading,stdlib,zlib,importlib,encodings,jit
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:
@@ -170,8 +176,9 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
name: Ensure compilation on various targets
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
target: i686-unknown-linux-gnu
@@ -211,13 +218,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
@@ -225,12 +225,13 @@ jobs:
- name: Check compilation for freeBSD
run: cargo check --target x86_64-unknown-freebsd
- name: Prepare repository for redox compilation
run: bash scripts/redox/uncomment-cargo.sh
- name: Check compilation for Redox
uses: coolreader18/redoxer-action@v1
with:
command: check
# - name: Prepare repository for redox compilation
# run: bash scripts/redox/uncomment-cargo.sh
# - name: Check compilation for Redox
# uses: coolreader18/redoxer-action@v1
# with:
# command: check
# args: --ignore-rust-version
snippets_cpython:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
@@ -238,30 +239,35 @@ jobs:
RUST_BACKTRACE: full
name: Run snippets and cpython tests
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ contains(matrix.os, 'windows') && 45 || 30 }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Set up the Windows environment
shell: bash
run: |
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 openssl@3
if: runner.os == 'macOS'
- name: build rustpython
run: cargo build --release --verbose --features=threading ${{ env.CARGO_ARGS }}
- uses: actions/setup-python@v4
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: ${{ env.PYTHON_VERSION }}
- name: run snippets
@@ -276,16 +282,17 @@ jobs:
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 all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.MACOS_SKIPS }}
run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
- if: runner.os == 'Windows'
name: run cpython platform-dependent tests (windows partial - fixme)
run:
target/release/rustpython -m test -j 1 all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }} ${{ env.WINDOWS_SKIPS }}
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
target/release/rustpython -m pip install six
- if: runner.os != 'Windows'
name: Check that ensurepip succeeds.
run: |
@@ -303,7 +310,7 @@ jobs:
name: Check Rust code with rustfmt and clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
@@ -311,42 +318,62 @@ jobs:
run: cargo fmt --check
- name: run clippy on wasm
run: cargo clippy --manifest-path=wasm/lib/Cargo.toml -- -Dwarnings
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install ruff
run: python -m pip install ruff==0.0.291 # astral-sh/ruff#7778
- name: run python lint
run: ruff extra_tests wasm examples --exclude='./.*',./Lib,./vm/Lib,./benches/ --select=E9,F63,F7,F82 --show-source
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
# 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
runs-on: ubuntu-latest
timeout-minutes: 30
env:
NIGHTLY_CHANNEL: nightly
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
toolchain: ${{ env.NIGHTLY_CHANNEL }}
components: miri
- uses: Swatinem/rust-cache@v2
- name: Run tests under miri
run: cargo +${{ env.NIGHTLY_CHANNEL }} miri test -p rustpython-vm -- miri_test
env:
# 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
run: MIRIFLAGS='-Zmiri-ignore-leaks' cargo +nightly miri test -p rustpython-vm -- miri_test
MIRIFLAGS: '-Zmiri-ignore-leaks'
wasm:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
name: Check the WASM package and demo
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
@@ -354,15 +381,18 @@ jobs:
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@v4
tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: python -m pip install -r requirements.txt
working-directory: ./wasm/tests
- uses: actions/setup-node@v3
- 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
@@ -371,6 +401,15 @@ jobs:
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: |
@@ -382,7 +421,7 @@ jobs:
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
@@ -393,18 +432,21 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
name: Run snippets and cpython tests on wasm-wasi
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
target: wasm32-wasi
target: wasm32-wasip1
- uses: Swatinem/rust-cache@v2
- name: Setup Wasmer
uses: wasmerio/setup-wasmer@v2
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 `pwd` target/wasm32-wasi/release/rustpython.wasm -- `pwd`/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

21
.github/workflows/comment-commands.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Comment Commands
on:
issue_comment:
types: created
jobs:
issue_assign:
if: (!github.event.issue.pull_request) && github.event.comment.body == 'take'
runs-on: ubuntu-latest
concurrency:
group: ${{ github.actor }}-issue-assign
permissions:
issues: write
steps:
# Using REST API and not `gh issue edit`. https://github.com/cli/cli/issues/6235#issuecomment-1243487651
- run: |
curl -H "Authorization: token ${{ github.token }}" -d '{"assignees": ["${{ github.event.comment.user.login }}"]}' https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/assignees

View File

@@ -2,12 +2,15 @@ on:
schedule:
- cron: '0 0 * * 6'
workflow_dispatch:
push:
paths:
- .github/workflows/cron-ci.yaml
name: Periodic checks/tasks
env:
CARGO_ARGS: --no-default-features --features stdlib,zlib,importlib,encodings,ssl,jit
PYTHON_VERSION: "3.12.0"
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.
@@ -16,15 +19,15 @@ jobs:
name: Collect code coverage data
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: sudo apt-get update && sudo apt-get -y install lcov
- name: Run cargo-llvm-cov with Rust tests.
run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --verbose --no-default-features --features stdlib,zlib,importlib,encodings,ssl,jit
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
@@ -34,7 +37,7 @@ jobs:
- name: Prepare code coverage data
run: cargo llvm-cov report --lcov --output-path='codecov.lcov'
- name: Upload to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v5
with:
file: ./codecov.lcov
@@ -42,7 +45,7 @@ jobs:
name: Collect regression test data
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: build rustpython
run: cargo build --release --verbose
@@ -71,9 +74,9 @@ jobs:
name: Collect what is left data
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: build rustpython
@@ -81,7 +84,7 @@ jobs:
- 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
@@ -97,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
@@ -106,9 +112,9 @@ jobs:
name: Collect benchmark data
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: 3.9
- run: cargo install cargo-criterion

173
.github/workflows/release.yml vendored Normal file
View 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-*

6
.gitignore vendored
View File

@@ -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

2612
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,115 +1,46 @@
[package]
name = "rustpython"
version = "0.3.1"
authors = ["RustPython Team"]
edition = "2021"
rust-version = "1.67.1"
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/core", "compiler/codegen",
".", "common", "derive", "jit", "vm", "vm/sre_engine", "pylib", "stdlib", "wasm/lib", "derive-impl",
]
[workspace.dependencies]
rustpython-compiler-core = { path = "compiler/core", version = "0.3.1" }
rustpython-compiler = { path = "compiler", version = "0.3.1" }
rustpython-codegen = { path = "compiler/codegen", version = "0.3.1" }
rustpython-common = { path = "common", version = "0.3.1" }
rustpython-derive = { path = "derive", version = "0.3.1" }
rustpython-derive-impl = { path = "derive-impl", version = "0.3.1" }
rustpython-jit = { path = "jit", version = "0.3.1" }
rustpython-vm = { path = "vm", default-features = false, version = "0.3.1" }
rustpython-pylib = { path = "pylib", version = "0.3.1" }
rustpython-stdlib = { path = "stdlib", default-features = false, version = "0.3.1" }
rustpython-sre_engine = { path = "vm/sre_engine", version = "0.3.1" }
rustpython-doc = { git = "https://github.com/RustPython/__doc__", tag = "0.3.0", version = "0.3.0" }
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", version = "0.3.1", rev = "a95045bc627b2fbf84caf4f010e521846be7b37f" }
rustpython-parser-core = { git = "https://github.com/RustPython/Parser.git", version = "0.3.1", rev = "a95045bc627b2fbf84caf4f010e521846be7b37f" }
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", version = "0.3.1", rev = "a95045bc627b2fbf84caf4f010e521846be7b37f" }
rustpython-ast = { git = "https://github.com/RustPython/Parser.git", version = "0.3.1", rev = "a95045bc627b2fbf84caf4f010e521846be7b37f" }
rustpython-format = { git = "https://github.com/RustPython/Parser.git", version = "0.3.1", rev = "a95045bc627b2fbf84caf4f010e521846be7b37f" }
# rustpython-literal = { path = "../RustPython-parser/literal" }
# rustpython-parser-core = { path = "../RustPython-parser/core" }
# rustpython-parser = { path = "../RustPython-parser/parser" }
# rustpython-ast = { path = "../RustPython-parser/ast" }
# rustpython-format = { path = "../RustPython-parser/format" }
ahash = "0.8.11"
ascii = "1.0"
atty = "0.2.14"
bitflags = "2.4.1"
bstr = "0.2.17"
cfg-if = "1.0"
chrono = "0.4.37"
crossbeam-utils = "0.8.19"
flame = "0.2.2"
glob = "0.3"
hex = "0.4.3"
indexmap = { version = "2.2.6", features = ["std"] }
insta = "1.38.0"
itertools = "0.11.0"
is-macro = "0.3.0"
libc = "0.2.153"
log = "0.4.16"
nix = { version = "0.27", features = ["fs", "user", "process", "term", "time", "signal", "ioctl", "socket", "sched", "zerocopy", "dir", "hostname", "net", "poll"] }
malachite-bigint = "0.2.0"
malachite-q = "0.4.4"
malachite-base = "0.4.4"
memchr = "2.7.2"
num-complex = "0.4.0"
num-integer = "0.1.44"
num-traits = "0.2"
num_enum = "0.7"
once_cell = "1.19.0"
parking_lot = "0.12.1"
paste = "1.0.7"
rand = "0.8.5"
rustyline = "14.0.0"
serde = { version = "1.0.133", default-features = false }
schannel = "0.1.22"
static_assertions = "1.1"
syn = "1.0.109"
thiserror = "1.0"
thread_local = "1.1.4"
unicode_names2 = "1.1.0"
widestring = "1.1.0"
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"]
default = ["threading", "stdlib", "stdio", "importlib"]
importlib = ["rustpython-vm/importlib"]
encodings = ["rustpython-vm/encodings"]
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"]
[build-dependencies]
winresource = "0.1"
[dependencies]
rustpython-compiler = { workspace = true }
rustpython-pylib = { workspace = true, optional = true }
rustpython-stdlib = { workspace = true, optional = true }
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
rustpython-vm = { workspace = true, features = ["compiler"] }
rustpython-parser = { workspace = true }
ruff_python_parser = { workspace = true }
atty = { workspace = true }
cfg-if = { workspace = true }
log = { workspace = true }
flame = { workspace = true, optional = true }
clap = "2.34"
dirs = { package = "dirs-next", version = "2.0.0" }
env_logger = { version = "0.9.0", default-features = false, features = ["atty", "termcolor"] }
lexopt = "0.3"
dirs = { package = "dirs-next", version = "2.0" }
env_logger = "0.11"
flamescope = { version = "0.1.2", optional = true }
[target.'cfg(windows)'.dependencies]
@@ -119,8 +50,8 @@ libc = { workspace = true }
rustyline = { workspace = true }
[dev-dependencies]
criterion = { version = "0.3.5", features = ["html_reports"] }
pyo3 = { version = "0.20.2", features = ["auto-initialize"] }
criterion = { workspace = true }
pyo3 = { version = "0.24", features = ["auto-initialize"] }
[[bench]]
name = "execution"
@@ -163,3 +94,148 @@ 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",
".",
"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-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.1"
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.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"
unicode-bidi-mirroring = "0.2"
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"

View File

@@ -25,7 +25,7 @@ RustPython requires the following:
stable version: `rustup update stable`
- If you do not have Rust installed, use [rustup](https://rustup.rs/) to
do so.
- CPython version 3.12 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

View File

@@ -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
View File

@@ -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)

108
Lib/_aix_support.py vendored Normal file
View 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
View 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
View 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)

View File

@@ -85,6 +85,10 @@ dict_values = type({}.values())
dict_items = type({}.items())
## misc ##
mappingproxy = type(type.__dict__)
def _get_framelocalsproxy():
return type(sys._getframe().f_locals)
framelocalsproxy = _get_framelocalsproxy()
del _get_framelocalsproxy
generator = type((lambda: (yield))())
## coroutine ##
async def _coro(): pass
@@ -508,6 +512,10 @@ class _CallableGenericAlias(GenericAlias):
new_args = (t_args, t_result)
return _CallableGenericAlias(Callable, tuple(new_args))
# TODO: RUSTPYTHON patch for common call
def __or__(self, other):
super().__or__(other)
def _is_param_expr(obj):
"""Checks if obj matches either a list of types, ``...``, ``ParamSpec`` or
``_ConcatenateGenericAlias`` from typing.py
@@ -836,6 +844,7 @@ class Mapping(Collection):
__reversed__ = None
Mapping.register(mappingproxy)
Mapping.register(framelocalsproxy)
class MappingView(Sized):
@@ -973,7 +982,7 @@ class MutableMapping(Mapping):
def update(self, other=(), /, **kwds):
''' D.update([E, ]**F) -> None. Update D from mapping/iterable E and F.
If E present and has a .keys() method, does: for k in E: D[k] = E[k]
If E present and has a .keys() method, does: for k in E.keys(): D[k] = E[k]
If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v
In either case, this is followed by: for k, v in F.items(): D[k] = v
'''

112
Lib/_colorize.py vendored Normal file
View File

@@ -0,0 +1,112 @@
from __future__ import annotations
import io
import os
import sys
COLORIZE = True
# types
if False:
from typing import IO
class ANSIColors:
RESET = "\x1b[0m"
BLACK = "\x1b[30m"
BLUE = "\x1b[34m"
CYAN = "\x1b[36m"
GREEN = "\x1b[32m"
MAGENTA = "\x1b[35m"
RED = "\x1b[31m"
WHITE = "\x1b[37m" # more like LIGHT GRAY
YELLOW = "\x1b[33m"
BOLD_BLACK = "\x1b[1;30m" # DARK GRAY
BOLD_BLUE = "\x1b[1;34m"
BOLD_CYAN = "\x1b[1;36m"
BOLD_GREEN = "\x1b[1;32m"
BOLD_MAGENTA = "\x1b[1;35m"
BOLD_RED = "\x1b[1;31m"
BOLD_WHITE = "\x1b[1;37m" # actual WHITE
BOLD_YELLOW = "\x1b[1;33m"
# intense = like bold but without being bold
INTENSE_BLACK = "\x1b[90m"
INTENSE_BLUE = "\x1b[94m"
INTENSE_CYAN = "\x1b[96m"
INTENSE_GREEN = "\x1b[92m"
INTENSE_MAGENTA = "\x1b[95m"
INTENSE_RED = "\x1b[91m"
INTENSE_WHITE = "\x1b[97m"
INTENSE_YELLOW = "\x1b[93m"
BACKGROUND_BLACK = "\x1b[40m"
BACKGROUND_BLUE = "\x1b[44m"
BACKGROUND_CYAN = "\x1b[46m"
BACKGROUND_GREEN = "\x1b[42m"
BACKGROUND_MAGENTA = "\x1b[45m"
BACKGROUND_RED = "\x1b[41m"
BACKGROUND_WHITE = "\x1b[47m"
BACKGROUND_YELLOW = "\x1b[43m"
INTENSE_BACKGROUND_BLACK = "\x1b[100m"
INTENSE_BACKGROUND_BLUE = "\x1b[104m"
INTENSE_BACKGROUND_CYAN = "\x1b[106m"
INTENSE_BACKGROUND_GREEN = "\x1b[102m"
INTENSE_BACKGROUND_MAGENTA = "\x1b[105m"
INTENSE_BACKGROUND_RED = "\x1b[101m"
INTENSE_BACKGROUND_WHITE = "\x1b[107m"
INTENSE_BACKGROUND_YELLOW = "\x1b[103m"
NoColors = ANSIColors()
for attr in dir(NoColors):
if not attr.startswith("__"):
setattr(NoColors, attr, "")
def get_colors(
colorize: bool = False, *, file: IO[str] | IO[bytes] | None = None
) -> ANSIColors:
if colorize or can_colorize(file=file):
return ANSIColors()
else:
return NoColors
def can_colorize(*, file: IO[str] | IO[bytes] | None = 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 hasattr(file, "isatty") and file.isatty()

22
Lib/_dummy_os.py vendored
View File

@@ -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):

71
Lib/_ios_support.py vendored Normal file
View 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

5
Lib/_osx_support.py vendored
View File

@@ -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

11
Lib/_pycodecs.py vendored
View File

@@ -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

File diff suppressed because it is too large Load Diff

286
Lib/_pydecimal.py vendored
View File

@@ -13,104 +13,7 @@
# bug) and will be backported. At this point the spec is stabilizing
# and the updates are becoming fewer, smaller, and less significant.
"""
This is an implementation of decimal floating point arithmetic based on
the General Decimal Arithmetic Specification:
http://speleotrove.com/decimal/decarith.html
and IEEE standard 854-1987:
http://en.wikipedia.org/wiki/IEEE_854-1987
Decimal floating point has finite precision with arbitrarily large bounds.
The purpose of this module is to support arithmetic using familiar
"schoolhouse" rules and to avoid some of the tricky representation
issues associated with binary floating point. The package is especially
useful for financial applications or for contexts where users have
expectations that are at odds with binary floating point (for instance,
in binary floating point, 1.00 % 0.1 gives 0.09999999999999995 instead
of 0.0; Decimal('1.00') % Decimal('0.1') returns the expected
Decimal('0.00')).
Here are some examples of using the decimal module:
>>> from decimal import *
>>> setcontext(ExtendedContext)
>>> Decimal(0)
Decimal('0')
>>> Decimal('1')
Decimal('1')
>>> Decimal('-.0123')
Decimal('-0.0123')
>>> Decimal(123456)
Decimal('123456')
>>> Decimal('123.45e12345678')
Decimal('1.2345E+12345680')
>>> Decimal('1.33') + Decimal('1.27')
Decimal('2.60')
>>> Decimal('12.34') + Decimal('3.87') - Decimal('18.41')
Decimal('-2.20')
>>> dig = Decimal(1)
>>> print(dig / Decimal(3))
0.333333333
>>> getcontext().prec = 18
>>> print(dig / Decimal(3))
0.333333333333333333
>>> print(dig.sqrt())
1
>>> print(Decimal(3).sqrt())
1.73205080756887729
>>> print(Decimal(3) ** 123)
4.85192780976896427E+58
>>> inf = Decimal(1) / Decimal(0)
>>> print(inf)
Infinity
>>> neginf = Decimal(-1) / Decimal(0)
>>> print(neginf)
-Infinity
>>> print(neginf + inf)
NaN
>>> print(neginf * inf)
-Infinity
>>> print(dig / 0)
Infinity
>>> getcontext().traps[DivisionByZero] = 1
>>> print(dig / 0)
Traceback (most recent call last):
...
...
...
decimal.DivisionByZero: x / 0
>>> c = Context()
>>> c.traps[InvalidOperation] = 0
>>> print(c.flags[InvalidOperation])
0
>>> c.divide(Decimal(0), Decimal(0))
Decimal('NaN')
>>> c.traps[InvalidOperation] = 1
>>> print(c.flags[InvalidOperation])
1
>>> c.flags[InvalidOperation] = 0
>>> print(c.flags[InvalidOperation])
0
>>> print(c.divide(Decimal(0), Decimal(0)))
Traceback (most recent call last):
...
...
...
decimal.InvalidOperation: 0 / 0
>>> print(c.flags[InvalidOperation])
1
>>> c.flags[InvalidOperation] = 0
>>> c.traps[InvalidOperation] = 0
>>> print(c.divide(Decimal(0), Decimal(0)))
NaN
>>> print(c.flags[InvalidOperation])
1
>>>
"""
"""Python decimal arithmetic module"""
__all__ = [
# Two major classes
@@ -140,8 +43,11 @@ __all__ = [
# Limits for the C version for compatibility
'MAX_PREC', 'MAX_EMAX', 'MIN_EMIN', 'MIN_ETINY',
# C version: compile time choice that enables the thread local context
'HAVE_THREADS'
# C version: compile time choice that enables the thread local context (deprecated, now always true)
'HAVE_THREADS',
# C version: compile time choice that enables the coroutine local context
'HAVE_CONTEXTVAR'
]
__xname__ = __name__ # sys.modules lookup (--without-threads)
@@ -156,7 +62,7 @@ import sys
try:
from collections import namedtuple as _namedtuple
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent')
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent', module='decimal')
except ImportError:
DecimalTuple = lambda *args: args
@@ -172,6 +78,7 @@ ROUND_05UP = 'ROUND_05UP'
# Compatibility with the C version
HAVE_THREADS = True
HAVE_CONTEXTVAR = True
if sys.maxsize == 2**63-1:
MAX_PREC = 999999999999999999
MAX_EMAX = 999999999999999999
@@ -190,7 +97,7 @@ class DecimalException(ArithmeticError):
Used exceptions derive from this.
If an exception derives from another exception besides this (such as
Underflow (Inexact, Rounded, Subnormal) that indicates that it is only
Underflow (Inexact, Rounded, Subnormal)) that indicates that it is only
called if the others are present. This isn't actually used for
anything, though.
@@ -238,7 +145,7 @@ class InvalidOperation(DecimalException):
x ** (+-)INF
An operand is invalid
The result of the operation after these is a quiet positive NaN,
The result of the operation after this is a quiet positive NaN,
except when the cause is a signaling NaN, in which case the result is
also a quiet NaN, but with the original sign, and an optional
diagnostic information.
@@ -431,82 +338,40 @@ _rounding_modes = (ROUND_DOWN, ROUND_HALF_UP, ROUND_HALF_EVEN, ROUND_CEILING,
##### Context Functions ##################################################
# The getcontext() and setcontext() function manage access to a thread-local
# current context. Py2.4 offers direct support for thread locals. If that
# is not available, use threading.current_thread() which is slower but will
# work for older Pythons. If threads are not part of the build, create a
# mock threading object with threading.local() returning the module namespace.
# current context.
try:
import threading
except ImportError:
# Python was compiled without threads; create a mock object instead
class MockThreading(object):
def local(self, sys=sys):
return sys.modules[__xname__]
threading = MockThreading()
del MockThreading
import contextvars
try:
threading.local
_current_context_var = contextvars.ContextVar('decimal_context')
except AttributeError:
_context_attributes = frozenset(
['prec', 'Emin', 'Emax', 'capitals', 'clamp', 'rounding', 'flags', 'traps']
)
# To fix reloading, force it to create a new context
# Old contexts have different exceptions in their dicts, making problems.
if hasattr(threading.current_thread(), '__decimal_context__'):
del threading.current_thread().__decimal_context__
def getcontext():
"""Returns this thread's context.
def setcontext(context):
"""Set this thread's context to context."""
if context in (DefaultContext, BasicContext, ExtendedContext):
context = context.copy()
context.clear_flags()
threading.current_thread().__decimal_context__ = context
If this thread does not yet have a context, returns
a new context and sets this thread's context.
New contexts are copies of DefaultContext.
"""
try:
return _current_context_var.get()
except LookupError:
context = Context()
_current_context_var.set(context)
return context
def getcontext():
"""Returns this thread's context.
def setcontext(context):
"""Set this thread's context to context."""
if context in (DefaultContext, BasicContext, ExtendedContext):
context = context.copy()
context.clear_flags()
_current_context_var.set(context)
If this thread does not yet have a context, returns
a new context and sets this thread's context.
New contexts are copies of DefaultContext.
"""
try:
return threading.current_thread().__decimal_context__
except AttributeError:
context = Context()
threading.current_thread().__decimal_context__ = context
return context
del contextvars # Don't contaminate the namespace
else:
local = threading.local()
if hasattr(local, '__decimal_context__'):
del local.__decimal_context__
def getcontext(_local=local):
"""Returns this thread's context.
If this thread does not yet have a context, returns
a new context and sets this thread's context.
New contexts are copies of DefaultContext.
"""
try:
return _local.__decimal_context__
except AttributeError:
context = Context()
_local.__decimal_context__ = context
return context
def setcontext(context, _local=local):
"""Set this thread's context to context."""
if context in (DefaultContext, BasicContext, ExtendedContext):
context = context.copy()
context.clear_flags()
_local.__decimal_context__ = context
del threading, local # Don't contaminate the namespace
def localcontext(ctx=None):
def localcontext(ctx=None, **kwargs):
"""Return a context manager for a copy of the supplied context
Uses a copy of the current context if no context is specified
@@ -542,8 +407,14 @@ def localcontext(ctx=None):
>>> print(getcontext().prec)
28
"""
if ctx is None: ctx = getcontext()
return _ContextManager(ctx)
if ctx is None:
ctx = getcontext()
ctx_manager = _ContextManager(ctx)
for key, value in kwargs.items():
if key not in _context_attributes:
raise TypeError(f"'{key}' is an invalid keyword argument for this function")
setattr(ctx_manager.new_context, key, value)
return ctx_manager
##### Decimal class #######################################################
@@ -553,7 +424,7 @@ def localcontext(ctx=None):
# numbers.py for more detail.
class Decimal(object):
"""Floating point class for decimal arithmetic."""
"""Floating-point class for decimal arithmetic."""
__slots__ = ('_exp','_int','_sign', '_is_special')
# Generally, the value of the Decimal instance is given by
@@ -734,18 +605,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:
@@ -988,7 +864,7 @@ class Decimal(object):
if self.is_snan():
raise TypeError('Cannot hash a signaling NaN value.')
elif self.is_nan():
return _PyHASH_NAN
return object.__hash__(self)
else:
if self._sign:
return -_PyHASH_INF
@@ -1669,13 +1545,13 @@ class Decimal(object):
__trunc__ = __int__
@property
def real(self):
return self
real = property(real)
@property
def imag(self):
return Decimal(0)
imag = property(imag)
def conjugate(self):
return self
@@ -2255,10 +2131,16 @@ class Decimal(object):
else:
return None
if xc >= 10**p:
# An exact power of 10 is representable, but can convert to a
# string of any length. But an exact power of 10 shouldn't be
# possible at this point.
assert xc > 1, self
assert xc % 10 != 0, self
strxc = str(xc)
if len(strxc) > p:
return None
xe = -e-xe
return _dec_from_triple(0, str(xc), xe)
return _dec_from_triple(0, strxc, xe)
# now y is positive; find m and n such that y = m/n
if ye >= 0:
@@ -2267,7 +2149,7 @@ class Decimal(object):
if xe != 0 and len(str(abs(yc*xe))) <= -ye:
return None
xc_bits = _nbits(xc)
if xc != 1 and len(str(abs(yc)*xc_bits)) <= -ye:
if len(str(abs(yc)*xc_bits)) <= -ye:
return None
m, n = yc, 10**(-ye)
while m % 2 == n % 2 == 0:
@@ -2280,7 +2162,7 @@ class Decimal(object):
# compute nth root of xc*10**xe
if n > 1:
# if 1 < xc < 2**n then xc isn't an nth power
if xc != 1 and xc_bits <= n:
if xc_bits <= n:
return None
xe, rem = divmod(xe, n)
@@ -2308,13 +2190,18 @@ class Decimal(object):
return None
xc = xc**m
xe *= m
if xc > 10**p:
# An exact power of 10 is representable, but can convert to a string
# of any length. But an exact power of 10 shouldn't be possible at
# this point.
assert xc > 1, self
assert xc % 10 != 0, self
str_xc = str(xc)
if len(str_xc) > p:
return None
# by this point the result *is* exactly representable
# adjust the exponent to get as close as possible to the ideal
# exponent, if necessary
str_xc = str(xc)
if other._isinteger() and other._sign == 0:
ideal_exponent = self._exp*int(other)
zeros = min(xe-ideal_exponent, p-len(str_xc))
@@ -3832,6 +3719,10 @@ class Decimal(object):
# represented in fixed point; rescale them to 0e0.
if not self and self._exp > 0 and spec['type'] in 'fF%':
self = self._rescale(0, rounding)
if not self and spec['no_neg_0'] and self._sign:
adjusted_sign = 0
else:
adjusted_sign = self._sign
# figure out placement of the decimal point
leftdigits = self._exp + len(self._int)
@@ -3862,7 +3753,7 @@ class Decimal(object):
# done with the decimal-specific stuff; hand over the rest
# of the formatting to the _format_number function
return _format_number(self._sign, intpart, fracpart, exp, spec)
return _format_number(adjusted_sign, intpart, fracpart, exp, spec)
def _dec_from_triple(sign, coefficient, exponent, special=False):
"""Create a decimal instance directly, without any validation,
@@ -5672,8 +5563,6 @@ class _WorkRep(object):
def __repr__(self):
return "(%r, %r, %r)" % (self.sign, self.int, self.exp)
__str__ = __repr__
def _normalize(op1, op2, prec = 0):
@@ -6182,7 +6071,7 @@ _exact_half = re.compile('50*$').match
#
# A format specifier for Decimal looks like:
#
# [[fill]align][sign][#][0][minimumwidth][,][.precision][type]
# [[fill]align][sign][z][#][0][minimumwidth][,][.precision][type]
_parse_format_specifier_regex = re.compile(r"""\A
(?:
@@ -6190,6 +6079,7 @@ _parse_format_specifier_regex = re.compile(r"""\A
(?P<align>[<>=^])
)?
(?P<sign>[-+ ])?
(?P<no_neg_0>z)?
(?P<alt>\#)?
(?P<zeropad>0)?
(?P<minimumwidth>(?!0)\d+)?

90
Lib/_pyio.py vendored
View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View File

@@ -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
View 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)

View File

@@ -4,133 +4,6 @@
class. Depending on the version of Python you're using, there may be a
faster one available. You should always import the `local` class from
`threading`.)
Thread-local objects support the management of thread-local data.
If you have data that you want to be local to a thread, simply create
a thread-local object and use its attributes:
>>> mydata = local()
>>> mydata.number = 42
>>> mydata.number
42
You can also access the local-object's dictionary:
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]
What's important about thread-local objects is that their data are
local to a thread. If we access the data in a different thread:
>>> log = []
>>> def f():
... items = sorted(mydata.__dict__.items())
... log.append(items)
... mydata.number = 11
... log.append(mydata.number)
>>> import threading
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
>>> log
[[], 11]
we get different data. Furthermore, changes made in the other thread
don't affect data seen in this thread:
>>> mydata.number
42
Of course, values you get from a local object, including a __dict__
attribute, are for whatever thread was current at the time the
attribute was read. For that reason, you generally don't want to save
these values across threads, as they apply only to the thread they
came from.
You can create custom local objects by subclassing the local class:
>>> class MyLocal(local):
... number = 2
... initialized = False
... def __init__(self, **kw):
... if self.initialized:
... raise SystemError('__init__ called too many times')
... self.initialized = True
... self.__dict__.update(kw)
... def squared(self):
... return self.number ** 2
This can be useful to support default values, methods and
initialization. Note that if you define an __init__ method, it will be
called each time the local object is used in a separate thread. This
is necessary to initialize each thread's dictionary.
Now if we create a local object:
>>> mydata = MyLocal(color='red')
Now we have a default number:
>>> mydata.number
2
an initial color:
>>> mydata.color
'red'
>>> del mydata.color
And a method that operates on the data:
>>> mydata.squared()
4
As before, we can access the data in a separate thread:
>>> log = []
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
>>> log
[[('color', 'red'), ('initialized', True)], 11]
without affecting this thread's data:
>>> mydata.number
2
>>> mydata.color
Traceback (most recent call last):
...
AttributeError: 'MyLocal' object has no attribute 'color'
Note that subclasses can define slots, but they are not thread
local. They are shared across threads:
>>> class MyLocal(local):
... __slots__ = 'number'
>>> mydata = MyLocal()
>>> mydata.number = 42
>>> mydata.color = 'red'
So, the separate thread:
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
affects what we see:
>>> # TODO: RUSTPYTHON, __slots__
>>> mydata.number #doctest: +SKIP
11
>>> del mydata
"""
from weakref import ref
@@ -194,7 +67,6 @@ class _localimpl:
@contextmanager
def _patch(self):
old = object.__getattribute__(self, '__dict__')
impl = object.__getattribute__(self, '_local__impl')
try:
dct = impl.get_dict()
@@ -205,13 +77,12 @@ def _patch(self):
with impl.locallock:
object.__setattr__(self, '__dict__', dct)
yield
object.__setattr__(self, '__dict__', old)
class local:
__slots__ = '_local__impl', '__dict__'
def __new__(cls, *args, **kw):
def __new__(cls, /, *args, **kw):
if (args or kw) and (cls.__init__ is object.__init__):
raise TypeError("Initialization arguments are not supported")
self = object.__new__(cls)

3
Lib/_weakrefset.py vendored
View File

@@ -80,8 +80,7 @@ class WeakSet:
return wr in self.data
def __reduce__(self):
return (self.__class__, (list(self),),
getattr(self, '__dict__', None))
return self.__class__, (list(self),), self.__getstate__()
def add(self, item):
if self._pending_removals:

4
Lib/abc.py vendored
View File

@@ -85,10 +85,6 @@ try:
from _abc import (get_cache_token, _abc_init, _abc_register,
_abc_instancecheck, _abc_subclasscheck, _get_dump,
_reset_registry, _reset_caches)
# TODO: RUSTPYTHON missing _abc module implementation.
except ModuleNotFoundError:
from _py_abc import ABCMeta, get_cache_token
ABCMeta.__module__ = 'abc'
except ImportError:
from _py_abc import ABCMeta, get_cache_token
ABCMeta.__module__ = 'abc'

984
Lib/aifc.py vendored
View File

@@ -1,984 +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"]
warnings._deprecated(__name__, remove=(3, 13))
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)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
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):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
return audioop.alaw2lin(data, 2)
def _ulaw2lin(self, data):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
return audioop.ulaw2lin(data, 2)
def _adpcm2lin(self, data):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
if not hasattr(self, '_adpcmstate'):
# first time
self._adpcmstate = None
data, self._adpcmstate = audioop.adpcm2lin(data, 2, self._adpcmstate)
return data
def _sowt2lin(self, data):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
return audioop.byteswap(data, 2)
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
elif self._comptype in (b'sowt', b'SOWT'):
self._convert = self._sowt2lin
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', b'sowt', b'SOWT'):
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', b'sowt', b'SOWT'):
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):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
return audioop.lin2alaw(data, 2)
def _lin2ulaw(self, data):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
return audioop.lin2ulaw(data, 2)
def _lin2adpcm(self, data):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
if not hasattr(self, '_adpcmstate'):
self._adpcmstate = None
data, self._adpcmstate = audioop.lin2adpcm(data, 2, self._adpcmstate)
return data
def _lin2sowt(self, data):
with warnings.catch_warnings():
warnings.simplefilter('ignore', category=DeprecationWarning)
import audioop
return audioop.byteswap(data, 2)
def _ensure_header_written(self, datasize):
if not self._nframeswritten:
if self._comptype in (b'ULAW', b'ulaw',
b'ALAW', b'alaw', b'G722',
b'sowt', b'SOWT'):
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, sowt/SOWT '
'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
elif self._comptype in (b'sowt', b'SOWT'):
self._convert = self._lin2sowt
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'")
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.")

591
Lib/argparse.py vendored

File diff suppressed because it is too large Load Diff

112
Lib/ast.py vendored
View File

@@ -1,28 +1,24 @@
"""
ast
~~~
The `ast` module helps Python applications to process trees of the Python
abstract syntax grammar. The abstract syntax itself might change with
each Python release; this module helps to find out programmatically what
the current grammar looks like and allows modifications of it.
The `ast` module helps Python applications to process trees of the Python
abstract syntax grammar. The abstract syntax itself might change with
each Python release; this module helps to find out programmatically what
the current grammar looks like and allows modifications of it.
An abstract syntax tree can be generated by passing `ast.PyCF_ONLY_AST` as
a flag to the `compile()` builtin function or by using the `parse()`
function from this module. The result will be a tree of objects whose
classes all inherit from `ast.AST`.
An abstract syntax tree can be generated by passing `ast.PyCF_ONLY_AST` as
a flag to the `compile()` builtin function or by using the `parse()`
function from this module. The result will be a tree of objects whose
classes all inherit from `ast.AST`.
A modified abstract syntax tree can be compiled into a Python code object
using the built-in `compile()` function.
A modified abstract syntax tree can be compiled into a Python code object
using the built-in `compile()` function.
Additionally various helper functions are provided that make working with
the trees simpler. The main intention of the helper functions and this
module in general is to provide an easy to use interface for libraries
that work tightly with the python syntax (template engines for example).
Additionally various helper functions are provided that make working with
the trees simpler. The main intention of the helper functions and this
module in general is to provide an easy to use interface for libraries
that work tightly with the python syntax (template engines for example).
:copyright: Copyright 2008 by Armin Ronacher.
:license: Python License.
:copyright: Copyright 2008 by Armin Ronacher.
:license: Python License.
"""
import sys
import re
@@ -32,13 +28,15 @@ from enum import IntEnum, auto, _simple_enum
def parse(source, filename='<unknown>', mode='exec', *,
type_comments=False, feature_version=None):
type_comments=False, feature_version=None, optimize=-1):
"""
Parse the source into an AST node.
Equivalent to compile(source, filename, mode, PyCF_ONLY_AST).
Pass type_comments=True to get back type comments where the syntax allows.
"""
flags = PyCF_ONLY_AST
if optimize > 0:
flags |= PyCF_OPTIMIZED_AST
if type_comments:
flags |= PyCF_TYPE_COMMENTS
if feature_version is None:
@@ -50,7 +48,7 @@ def parse(source, filename='<unknown>', mode='exec', *,
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)
_feature_version=feature_version, optimize=optimize)
def literal_eval(node_or_string):
@@ -112,7 +110,11 @@ def literal_eval(node_or_string):
return _convert(node_or_string)
def dump(node, annotate_fields=True, include_attributes=False, *, indent=None):
def dump(
node, annotate_fields=True, include_attributes=False,
*,
indent=None, show_empty=False,
):
"""
Return a formatted dump of the tree in node. This is mainly useful for
debugging purposes. If annotate_fields is true (by default),
@@ -123,6 +125,8 @@ def dump(node, annotate_fields=True, include_attributes=False, *, indent=None):
include_attributes can be set to true. If indent is a non-negative
integer or string, then the tree will be pretty-printed with that indent
level. None (the default) selects the single line representation.
If show_empty is False, then empty lists and fields that are None
will be omitted from the output for better readability.
"""
def _format(node, level=0):
if indent is not None:
@@ -135,6 +139,7 @@ def dump(node, annotate_fields=True, include_attributes=False, *, indent=None):
if isinstance(node, AST):
cls = type(node)
args = []
args_buffer = []
allsimple = True
keywords = annotate_fields
for name in node._fields:
@@ -146,6 +151,16 @@ def dump(node, annotate_fields=True, include_attributes=False, *, indent=None):
if value is None and getattr(cls, name, ...) is None:
keywords = True
continue
if not show_empty:
if value == []:
field_type = cls._field_types.get(name, object)
if getattr(field_type, '__origin__', ...) is list:
if not keywords:
args_buffer.append(repr(value))
continue
if not keywords:
args.extend(args_buffer)
args_buffer = []
value, simple = _format(value, level)
allsimple = allsimple and simple
if keywords:
@@ -726,12 +741,11 @@ class _Unparser(NodeVisitor):
output source code for the abstract syntax; original formatting
is disregarded."""
def __init__(self, *, _avoid_backslashes=False):
def __init__(self):
self._source = []
self._precedences = {}
self._type_ignores = {}
self._indent = 0
self._avoid_backslashes = _avoid_backslashes
self._in_try_star = False
def interleave(self, inter, f, seq):
@@ -1104,12 +1118,21 @@ class _Unparser(NodeVisitor):
if node.bound:
self.write(": ")
self.traverse(node.bound)
if node.default_value:
self.write(" = ")
self.traverse(node.default_value)
def visit_TypeVarTuple(self, node):
self.write("*" + node.name)
if node.default_value:
self.write(" = ")
self.traverse(node.default_value)
def visit_ParamSpec(self, node):
self.write("**" + node.name)
if node.default_value:
self.write(" = ")
self.traverse(node.default_value)
def visit_TypeAlias(self, node):
self.fill("type ")
@@ -1246,9 +1269,14 @@ class _Unparser(NodeVisitor):
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
else:
if "\n" in value:
quote_types = [q for q in quote_types if q in _MULTI_QUOTES]
assert quote_types
new_quote_types = [q for q in quote_types if q not in value]
if new_quote_types:
quote_types = new_quote_types
new_fstring_parts.append(value)
if fallback_to_repr:
@@ -1268,13 +1296,19 @@ class _Unparser(NodeVisitor):
quote_type = quote_types[0]
self.write(f"{quote_type}{value}{quote_type}")
def _write_fstring_inner(self, node):
def _write_fstring_inner(self, node, is_format_spec=False):
if isinstance(node, JoinedStr):
# for both the f-string itself, and format_spec
for value in node.values:
self._write_fstring_inner(value)
self._write_fstring_inner(value, is_format_spec=is_format_spec)
elif isinstance(node, Constant) and isinstance(node.value, str):
value = node.value.replace("{", "{{").replace("}", "}}")
if is_format_spec:
value = value.replace("\\", "\\\\")
value = value.replace("'", "\\'")
value = value.replace('"', '\\"')
value = value.replace("\n", "\\n")
self.write(value)
elif isinstance(node, FormattedValue):
self.visit_FormattedValue(node)
@@ -1297,7 +1331,7 @@ class _Unparser(NodeVisitor):
self.write(f"!{chr(node.conversion)}")
if node.format_spec:
self.write(":")
self._write_fstring_inner(node.format_spec)
self._write_fstring_inner(node.format_spec, is_format_spec=True)
def visit_Name(self, node):
self.write(node.id)
@@ -1317,8 +1351,6 @@ class _Unparser(NodeVisitor):
.replace("inf", _INFSTR)
.replace("nan", f"({_INFSTR}-{_INFSTR})")
)
elif self._avoid_backslashes and isinstance(value, str):
self._write_str_avoiding_backslashes(value)
else:
self.write(repr(value))
@@ -1805,8 +1837,7 @@ def main():
import argparse
parser = argparse.ArgumentParser(prog='python -m ast')
parser.add_argument('infile', type=argparse.FileType(mode='rb'), nargs='?',
default='-',
parser.add_argument('infile', nargs='?', default='-',
help='the file to parse; defaults to stdin')
parser.add_argument('-m', '--mode', default='exec',
choices=('exec', 'single', 'eval', 'func_type'),
@@ -1820,9 +1851,14 @@ def main():
help='indentation of nodes (number of spaces)')
args = parser.parse_args()
with args.infile as infile:
source = infile.read()
tree = parse(source, args.infile.name, args.mode, type_comments=args.no_type_comments)
if args.infile == '-':
name = '<stdin>'
source = sys.stdin.buffer.read()
else:
name = args.infile
with open(args.infile, 'rb') as infile:
source = infile.read()
tree = parse(source, name, args.mode, type_comments=args.no_type_comments)
print(dump(tree, include_attributes=args.include_attributes, indent=args.indent))
if __name__ == '__main__':

307
Lib/asynchat.py vendored
View File

@@ -1,307 +0,0 @@
# -*- Mode: Python; tab-width: 4 -*-
# Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp
# Author: Sam Rushing <rushing@nightmare.com>
# ======================================================================
# Copyright 1996 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING 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.
# ======================================================================
r"""A class supporting chat-style (command/response) protocols.
This class adds support for 'chat' style protocols - where one side
sends a 'command', and the other sends a response (examples would be
the common internet protocols - smtp, nntp, ftp, etc..).
The handle_read() method looks at the input stream for the current
'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n'
for multi-line output), calling self.found_terminator() on its
receipt.
for example:
Say you build an async nntp client using this class. At the start
of the connection, you'll have self.terminator set to '\r\n', in
order to process the single-line greeting. Just before issuing a
'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST
command will be accumulated (using your own 'collect_incoming_data'
method) up to the terminator, and then control will be returned to
you - by calling your self.found_terminator() method.
"""
import asyncore
from collections import deque
class async_chat(asyncore.dispatcher):
"""This is an abstract class. You must derive from this class, and add
the two methods collect_incoming_data() and found_terminator()"""
# these are overridable defaults
ac_in_buffer_size = 65536
ac_out_buffer_size = 65536
# we don't want to enable the use of encoding by default, because that is a
# sign of an application bug that we don't want to pass silently
use_encoding = 0
encoding = 'latin-1'
def __init__(self, sock=None, map=None):
# for string terminator matching
self.ac_in_buffer = b''
# we use a list here rather than io.BytesIO for a few reasons...
# del lst[:] is faster than bio.truncate(0)
# lst = [] is faster than bio.truncate(0)
self.incoming = []
# we toss the use of the "simple producer" and replace it with
# a pure deque, which the original fifo was a wrapping of
self.producer_fifo = deque()
asyncore.dispatcher.__init__(self, sock, map)
def collect_incoming_data(self, data):
raise NotImplementedError("must be implemented in subclass")
def _collect_incoming_data(self, data):
self.incoming.append(data)
def _get_data(self):
d = b''.join(self.incoming)
del self.incoming[:]
return d
def found_terminator(self):
raise NotImplementedError("must be implemented in subclass")
def set_terminator(self, term):
"""Set the input delimiter.
Can be a fixed string of any length, an integer, or None.
"""
if isinstance(term, str) and self.use_encoding:
term = bytes(term, self.encoding)
elif isinstance(term, int) and term < 0:
raise ValueError('the number of received bytes must be positive')
self.terminator = term
def get_terminator(self):
return self.terminator
# grab some more data from the socket,
# throw it to the collector method,
# check for the terminator,
# if found, transition to the next state.
def handle_read(self):
try:
data = self.recv(self.ac_in_buffer_size)
except BlockingIOError:
return
except OSError as why:
self.handle_error()
return
if isinstance(data, str) and self.use_encoding:
data = bytes(str, self.encoding)
self.ac_in_buffer = self.ac_in_buffer + data
# Continue to search for self.terminator in self.ac_in_buffer,
# while calling self.collect_incoming_data. The while loop
# is necessary because we might read several data+terminator
# combos with a single recv(4096).
while self.ac_in_buffer:
lb = len(self.ac_in_buffer)
terminator = self.get_terminator()
if not terminator:
# no terminator, collect it all
self.collect_incoming_data(self.ac_in_buffer)
self.ac_in_buffer = b''
elif isinstance(terminator, int):
# numeric terminator
n = terminator
if lb < n:
self.collect_incoming_data(self.ac_in_buffer)
self.ac_in_buffer = b''
self.terminator = self.terminator - lb
else:
self.collect_incoming_data(self.ac_in_buffer[:n])
self.ac_in_buffer = self.ac_in_buffer[n:]
self.terminator = 0
self.found_terminator()
else:
# 3 cases:
# 1) end of buffer matches terminator exactly:
# collect data, transition
# 2) end of buffer matches some prefix:
# collect data to the prefix
# 3) end of buffer does not match any prefix:
# collect data
terminator_len = len(terminator)
index = self.ac_in_buffer.find(terminator)
if index != -1:
# we found the terminator
if index > 0:
# don't bother reporting the empty string
# (source of subtle bugs)
self.collect_incoming_data(self.ac_in_buffer[:index])
self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:]
# This does the Right Thing if the terminator
# is changed here.
self.found_terminator()
else:
# check for a prefix of the terminator
index = find_prefix_at_end(self.ac_in_buffer, terminator)
if index:
if index != lb:
# we found a prefix, collect up to the prefix
self.collect_incoming_data(self.ac_in_buffer[:-index])
self.ac_in_buffer = self.ac_in_buffer[-index:]
break
else:
# no prefix, collect it all
self.collect_incoming_data(self.ac_in_buffer)
self.ac_in_buffer = b''
def handle_write(self):
self.initiate_send()
def handle_close(self):
self.close()
def push(self, data):
if not isinstance(data, (bytes, bytearray, memoryview)):
raise TypeError('data argument must be byte-ish (%r)',
type(data))
sabs = self.ac_out_buffer_size
if len(data) > sabs:
for i in range(0, len(data), sabs):
self.producer_fifo.append(data[i:i+sabs])
else:
self.producer_fifo.append(data)
self.initiate_send()
def push_with_producer(self, producer):
self.producer_fifo.append(producer)
self.initiate_send()
def readable(self):
"predicate for inclusion in the readable for select()"
# cannot use the old predicate, it violates the claim of the
# set_terminator method.
# return (len(self.ac_in_buffer) <= self.ac_in_buffer_size)
return 1
def writable(self):
"predicate for inclusion in the writable for select()"
return self.producer_fifo or (not self.connected)
def close_when_done(self):
"automatically close this channel once the outgoing queue is empty"
self.producer_fifo.append(None)
def initiate_send(self):
while self.producer_fifo and self.connected:
first = self.producer_fifo[0]
# handle empty string/buffer or None entry
if not first:
del self.producer_fifo[0]
if first is None:
self.handle_close()
return
# handle classic producer behavior
obs = self.ac_out_buffer_size
try:
data = first[:obs]
except TypeError:
data = first.more()
if data:
self.producer_fifo.appendleft(data)
else:
del self.producer_fifo[0]
continue
if isinstance(data, str) and self.use_encoding:
data = bytes(data, self.encoding)
# send the data
try:
num_sent = self.send(data)
except OSError:
self.handle_error()
return
if num_sent:
if num_sent < len(data) or obs < len(first):
self.producer_fifo[0] = first[num_sent:]
else:
del self.producer_fifo[0]
# we tried to send some actual data
return
def discard_buffers(self):
# Emergencies only!
self.ac_in_buffer = b''
del self.incoming[:]
self.producer_fifo.clear()
class simple_producer:
def __init__(self, data, buffer_size=512):
self.data = data
self.buffer_size = buffer_size
def more(self):
if len(self.data) > self.buffer_size:
result = self.data[:self.buffer_size]
self.data = self.data[self.buffer_size:]
return result
else:
result = self.data
self.data = b''
return result
# Given 'haystack', see if any prefix of 'needle' is at its end. This
# assumes an exact match has already been checked. Return the number of
# characters matched.
# for example:
# f_p_a_e("qwerty\r", "\r\n") => 1
# f_p_a_e("qwertydkjf", "\r\n") => 0
# f_p_a_e("qwerty\r\n", "\r\n") => <undefined>
# this could maybe be made faster with a computed regex?
# [answer: no; circa Python-2.0, Jan 2001]
# new python: 28961/s
# old python: 18307/s
# re: 12820/s
# regex: 14035/s
def find_prefix_at_end(haystack, needle):
l = len(needle) - 1
while l and not haystack.endswith(needle[:l]):
l -= 1
return l

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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}>'

View File

@@ -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

View File

@@ -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
View File

@@ -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)

View File

@@ -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()

View File

@@ -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
View File

@@ -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
View 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
View 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
View File

@@ -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
View File

@@ -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
View 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

View File

@@ -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()

View File

@@ -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
View File

@@ -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
View File

@@ -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():

File diff suppressed because it is too large Load Diff

1136
Lib/asyncio/sslproto.py vendored

File diff suppressed because it is too large Load Diff

149
Lib/asyncio/staggered.py vendored Normal file
View 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
View File

@@ -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

View File

@@ -1,4 +1,4 @@
__all__ = ['create_subprocess_exec', 'create_subprocess_shell']
__all__ = 'create_subprocess_exec', 'create_subprocess_shell'
import subprocess
@@ -6,7 +6,6 @@ from . import events
from . import protocols
from . import streams
from . import tasks
from .coroutines import coroutine
from .log import logger
@@ -24,16 +23,19 @@ class SubprocessStreamProtocol(streams.FlowControlMixin,
self._limit = limit
self.stdin = self.stdout = self.stderr = None
self._transport = None
self._process_exited = False
self._pipe_fds = []
self._stdin_closed = self._loop.create_future()
def __repr__(self):
info = [self.__class__.__name__]
if self.stdin is not None:
info.append('stdin=%r' % self.stdin)
info.append(f'stdin={self.stdin!r}')
if self.stdout is not None:
info.append('stdout=%r' % self.stdout)
info.append(f'stdout={self.stdout!r}')
if self.stderr is not None:
info.append('stderr=%r' % self.stderr)
return '<%s>' % ' '.join(info)
info.append(f'stderr={self.stderr!r}')
return '<{}>'.format(' '.join(info))
def connection_made(self, transport):
self._transport = transport
@@ -43,12 +45,14 @@ class SubprocessStreamProtocol(streams.FlowControlMixin,
self.stdout = streams.StreamReader(limit=self._limit,
loop=self._loop)
self.stdout.set_transport(stdout_transport)
self._pipe_fds.append(1)
stderr_transport = transport.get_pipe_transport(2)
if stderr_transport is not None:
self.stderr = streams.StreamReader(limit=self._limit,
loop=self._loop)
self.stderr.set_transport(stderr_transport)
self._pipe_fds.append(2)
stdin_transport = transport.get_pipe_transport(0)
if stdin_transport is not None:
@@ -73,6 +77,13 @@ class SubprocessStreamProtocol(streams.FlowControlMixin,
if pipe is not None:
pipe.close()
self.connection_lost(exc)
if exc is None:
self._stdin_closed.set_result(None)
else:
self._stdin_closed.set_exception(exc)
# Since calling `wait_closed()` is not mandatory,
# we shouldn't log the traceback if this is not awaited.
self._stdin_closed._log_traceback = False
return
if fd == 1:
reader = self.stdout
@@ -80,15 +91,28 @@ class SubprocessStreamProtocol(streams.FlowControlMixin,
reader = self.stderr
else:
reader = None
if reader != None:
if reader is not None:
if exc is None:
reader.feed_eof()
else:
reader.set_exception(exc)
if fd in self._pipe_fds:
self._pipe_fds.remove(fd)
self._maybe_close_transport()
def process_exited(self):
self._transport.close()
self._transport = None
self._process_exited = True
self._maybe_close_transport()
def _maybe_close_transport(self):
if len(self._pipe_fds) == 0 and self._process_exited:
self._transport.close()
self._transport = None
def _get_close_waiter(self, stream):
if stream is self.stdin:
return self._stdin_closed
class Process:
@@ -102,18 +126,15 @@ class Process:
self.pid = transport.get_pid()
def __repr__(self):
return '<%s %s>' % (self.__class__.__name__, self.pid)
return f'<{self.__class__.__name__} {self.pid}>'
@property
def returncode(self):
return self._transport.get_returncode()
@coroutine
def wait(self):
"""Wait until the process exit and return the process return code.
This method is a coroutine."""
return (yield from self._transport._wait())
async def wait(self):
"""Wait until the process exit and return the process return code."""
return await self._transport._wait()
def send_signal(self, signal):
self._transport.send_signal(signal)
@@ -124,17 +145,19 @@ class Process:
def kill(self):
self._transport.kill()
@coroutine
def _feed_stdin(self, input):
async def _feed_stdin(self, input):
debug = self._loop.get_debug()
self.stdin.write(input)
if debug:
logger.debug('%r communicate: feed stdin (%s bytes)',
self, len(input))
try:
yield from self.stdin.drain()
if input is not None:
self.stdin.write(input)
if debug:
logger.debug(
'%r communicate: feed stdin (%s bytes)', self, len(input))
await self.stdin.drain()
except (BrokenPipeError, ConnectionResetError) as exc:
# communicate() ignores BrokenPipeError and ConnectionResetError
# communicate() ignores BrokenPipeError and ConnectionResetError.
# write() and drain() can raise these exceptions.
if debug:
logger.debug('%r communicate: stdin got %r', self, exc)
@@ -142,12 +165,10 @@ class Process:
logger.debug('%r communicate: close stdin', self)
self.stdin.close()
@coroutine
def _noop(self):
async def _noop(self):
return None
@coroutine
def _read_stream(self, fd):
async def _read_stream(self, fd):
transport = self._transport.get_pipe_transport(fd)
if fd == 2:
stream = self.stderr
@@ -157,16 +178,15 @@ class Process:
if self._loop.get_debug():
name = 'stdout' if fd == 1 else 'stderr'
logger.debug('%r communicate: read %s', self, name)
output = yield from stream.read()
output = await stream.read()
if self._loop.get_debug():
name = 'stdout' if fd == 1 else 'stderr'
logger.debug('%r communicate: close %s', self, name)
transport.close()
return output
@coroutine
def communicate(self, input=None):
if input is not None:
async def communicate(self, input=None):
if self.stdin is not None:
stdin = self._feed_stdin(input)
else:
stdin = self._noop()
@@ -178,36 +198,32 @@ class Process:
stderr = self._read_stream(2)
else:
stderr = self._noop()
stdin, stdout, stderr = yield from tasks.gather(stdin, stdout, stderr,
loop=self._loop)
yield from self.wait()
stdin, stdout, stderr = await tasks.gather(stdin, stdout, stderr)
await self.wait()
return (stdout, stderr)
@coroutine
def create_subprocess_shell(cmd, stdin=None, stdout=None, stderr=None,
loop=None, limit=streams._DEFAULT_LIMIT, **kwds):
if loop is None:
loop = events.get_event_loop()
async def create_subprocess_shell(cmd, stdin=None, stdout=None, stderr=None,
limit=streams._DEFAULT_LIMIT, **kwds):
loop = events.get_running_loop()
protocol_factory = lambda: SubprocessStreamProtocol(limit=limit,
loop=loop)
transport, protocol = yield from loop.subprocess_shell(
protocol_factory,
cmd, stdin=stdin, stdout=stdout,
stderr=stderr, **kwds)
transport, protocol = await loop.subprocess_shell(
protocol_factory,
cmd, stdin=stdin, stdout=stdout,
stderr=stderr, **kwds)
return Process(transport, protocol, loop)
@coroutine
def create_subprocess_exec(program, *args, stdin=None, stdout=None,
stderr=None, loop=None,
limit=streams._DEFAULT_LIMIT, **kwds):
if loop is None:
loop = events.get_event_loop()
async def create_subprocess_exec(program, *args, stdin=None, stdout=None,
stderr=None, limit=streams._DEFAULT_LIMIT,
**kwds):
loop = events.get_running_loop()
protocol_factory = lambda: SubprocessStreamProtocol(limit=limit,
loop=loop)
transport, protocol = yield from loop.subprocess_exec(
protocol_factory,
program, *args,
stdin=stdin, stdout=stdout,
stderr=stderr, **kwds)
transport, protocol = await loop.subprocess_exec(
protocol_factory,
program, *args,
stdin=stdin, stdout=stdout,
stderr=stderr, **kwds)
return Process(transport, protocol, loop)

240
Lib/asyncio/taskgroups.py vendored Normal file
View File

@@ -0,0 +1,240 @@
# Adapted with permission from the EdgeDB project;
# license: PSFL.
__all__ = ("TaskGroup",)
from . import events
from . import exceptions
from . import tasks
class TaskGroup:
"""Asynchronous context manager for managing groups of tasks.
Example use:
async with asyncio.TaskGroup() as group:
task1 = group.create_task(some_coroutine(...))
task2 = group.create_task(other_coroutine(...))
print("Both tasks have completed now.")
All tasks are awaited when the context manager exits.
Any exceptions other than `asyncio.CancelledError` raised within
a task will cancel all remaining tasks and wait for them to exit.
The exceptions are then combined and raised as an `ExceptionGroup`.
"""
def __init__(self):
self._entered = False
self._exiting = False
self._aborting = False
self._loop = None
self._parent_task = None
self._parent_cancel_requested = False
self._tasks = set()
self._errors = []
self._base_error = None
self._on_completed_fut = None
def __repr__(self):
info = ['']
if self._tasks:
info.append(f'tasks={len(self._tasks)}')
if self._errors:
info.append(f'errors={len(self._errors)}')
if self._aborting:
info.append('cancelling')
elif self._entered:
info.append('entered')
info_str = ' '.join(info)
return f'<TaskGroup{info_str}>'
async def __aenter__(self):
if self._entered:
raise RuntimeError(
f"TaskGroup {self!r} has already been entered")
if self._loop is None:
self._loop = events.get_running_loop()
self._parent_task = tasks.current_task(self._loop)
if self._parent_task is None:
raise RuntimeError(
f'TaskGroup {self!r} cannot determine the parent task')
self._entered = True
return self
async def __aexit__(self, et, exc, tb):
self._exiting = True
if (exc is not None and
self._is_base_error(exc) and
self._base_error is None):
self._base_error = exc
propagate_cancellation_error = \
exc if et is exceptions.CancelledError else None
if self._parent_cancel_requested:
# If this flag is set we *must* call uncancel().
if self._parent_task.uncancel() == 0:
# If there are no pending cancellations left,
# don't propagate CancelledError.
propagate_cancellation_error = None
if et is not None:
if not self._aborting:
# Our parent task is being cancelled:
#
# async with TaskGroup() as g:
# g.create_task(...)
# await ... # <- CancelledError
#
# or there's an exception in "async with":
#
# async with TaskGroup() as g:
# g.create_task(...)
# 1 / 0
#
self._abort()
# We use while-loop here because "self._on_completed_fut"
# can be cancelled multiple times if our parent task
# is being cancelled repeatedly (or even once, when
# our own cancellation is already in progress)
while self._tasks:
if self._on_completed_fut is None:
self._on_completed_fut = self._loop.create_future()
try:
await self._on_completed_fut
except exceptions.CancelledError as ex:
if not self._aborting:
# Our parent task is being cancelled:
#
# async def wrapper():
# async with TaskGroup() as g:
# g.create_task(foo)
#
# "wrapper" is being cancelled while "foo" is
# still running.
propagate_cancellation_error = ex
self._abort()
self._on_completed_fut = None
assert not self._tasks
if self._base_error is not None:
raise self._base_error
# Propagate CancelledError if there is one, except if there
# are other errors -- those have priority.
if propagate_cancellation_error and not self._errors:
raise propagate_cancellation_error
if et is not None and et is not exceptions.CancelledError:
self._errors.append(exc)
if self._errors:
# Exceptions are heavy objects that can have object
# cycles (bad for GC); let's not keep a reference to
# a bunch of them.
try:
me = BaseExceptionGroup('unhandled errors in a TaskGroup', self._errors)
raise me from None
finally:
self._errors = None
def create_task(self, coro, *, name=None, context=None):
"""Create a new task in this group and return it.
Similar to `asyncio.create_task`.
"""
if not self._entered:
raise RuntimeError(f"TaskGroup {self!r} has not been entered")
if self._exiting and not self._tasks:
raise RuntimeError(f"TaskGroup {self!r} is finished")
if self._aborting:
raise RuntimeError(f"TaskGroup {self!r} is shutting down")
if context is None:
task = self._loop.create_task(coro)
else:
task = self._loop.create_task(coro, context=context)
tasks._set_task_name(task, name)
# optimization: Immediately call the done callback if the task is
# already done (e.g. if the coro was able to complete eagerly),
# and skip scheduling a done callback
if task.done():
self._on_task_done(task)
else:
self._tasks.add(task)
task.add_done_callback(self._on_task_done)
return task
# Since Python 3.8 Tasks propagate all exceptions correctly,
# except for KeyboardInterrupt and SystemExit which are
# still considered special.
def _is_base_error(self, exc: BaseException) -> bool:
assert isinstance(exc, BaseException)
return isinstance(exc, (SystemExit, KeyboardInterrupt))
def _abort(self):
self._aborting = True
for t in self._tasks:
if not t.done():
t.cancel()
def _on_task_done(self, task):
self._tasks.discard(task)
if self._on_completed_fut is not None and not self._tasks:
if not self._on_completed_fut.done():
self._on_completed_fut.set_result(True)
if task.cancelled():
return
exc = task.exception()
if exc is None:
return
self._errors.append(exc)
if self._is_base_error(exc) and self._base_error is None:
self._base_error = exc
if self._parent_task.done():
# Not sure if this case is possible, but we want to handle
# it anyways.
self._loop.call_exception_handler({
'message': f'Task {task!r} has errored out but its parent '
f'task {self._parent_task} is already completed',
'exception': exc,
'task': task,
})
return
if not self._aborting and not self._parent_cancel_requested:
# If parent task *is not* being cancelled, it means that we want
# to manually cancel it to abort whatever is being run right now
# in the TaskGroup. But we want to mark parent task as
# "not cancelled" later in __aexit__. Example situation that
# we need to handle:
#
# async def foo():
# try:
# async with TaskGroup() as g:
# g.create_task(crash_soon())
# await something # <- this needs to be canceled
# # by the TaskGroup, e.g.
# # foo() needs to be cancelled
# except Exception:
# # Ignore any exceptions raised in the TaskGroup
# pass
# await something_else # this line has to be called
# # after TaskGroup is finished.
self._abort()
self._parent_cancel_requested = True
self._parent_task.cancel()

853
Lib/asyncio/tasks.py vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,503 +0,0 @@
"""Utilities shared by tests."""
import collections
import contextlib
import io
import logging
import os
import re
import socket
import socketserver
import sys
import tempfile
import threading
import time
import unittest
import weakref
from unittest import mock
from http.server import HTTPServer
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer
try:
import ssl
except ImportError: # pragma: no cover
ssl = None
from . import base_events
from . import compat
from . import events
from . import futures
from . import selectors
from . import tasks
from .coroutines import coroutine
from .log import logger
if sys.platform == 'win32': # pragma: no cover
from .windows_utils import socketpair
else:
from socket import socketpair # pragma: no cover
def dummy_ssl_context():
if ssl is None:
return None
else:
return ssl.SSLContext(ssl.PROTOCOL_SSLv23)
def run_briefly(loop):
@coroutine
def once():
pass
gen = once()
t = loop.create_task(gen)
# Don't log a warning if the task is not done after run_until_complete().
# It occurs if the loop is stopped or if a task raises a BaseException.
t._log_destroy_pending = False
try:
loop.run_until_complete(t)
finally:
gen.close()
def run_until(loop, pred, timeout=30):
deadline = time.time() + timeout
while not pred():
if timeout is not None:
timeout = deadline - time.time()
if timeout <= 0:
raise futures.TimeoutError()
loop.run_until_complete(tasks.sleep(0.001, loop=loop))
def run_once(loop):
"""Legacy API to run once through the event loop.
This is the recommended pattern for test code. It will poll the
selector once and run all callbacks scheduled in response to I/O
events.
"""
loop.call_soon(loop.stop)
loop.run_forever()
class SilentWSGIRequestHandler(WSGIRequestHandler):
def get_stderr(self):
return io.StringIO()
def log_message(self, format, *args):
pass
class SilentWSGIServer(WSGIServer):
request_timeout = 2
def get_request(self):
request, client_addr = super().get_request()
request.settimeout(self.request_timeout)
return request, client_addr
def handle_error(self, request, client_address):
pass
class SSLWSGIServerMixin:
def finish_request(self, request, client_address):
# The relative location of our test directory (which
# contains the ssl key and certificate files) differs
# between the stdlib and stand-alone asyncio.
# Prefer our own if we can find it.
here = os.path.join(os.path.dirname(__file__), '..', 'tests')
if not os.path.isdir(here):
here = os.path.join(os.path.dirname(os.__file__),
'test', 'test_asyncio')
keyfile = os.path.join(here, 'ssl_key.pem')
certfile = os.path.join(here, 'ssl_cert.pem')
context = ssl.SSLContext()
context.load_cert_chain(certfile, keyfile)
ssock = context.wrap_socket(request, server_side=True)
try:
self.RequestHandlerClass(ssock, client_address, self)
ssock.close()
except OSError:
# maybe socket has been closed by peer
pass
class SSLWSGIServer(SSLWSGIServerMixin, SilentWSGIServer):
pass
def _run_test_server(*, address, use_ssl=False, server_cls, server_ssl_cls):
def app(environ, start_response):
status = '200 OK'
headers = [('Content-type', 'text/plain')]
start_response(status, headers)
return [b'Test message']
# Run the test WSGI server in a separate thread in order not to
# interfere with event handling in the main thread
server_class = server_ssl_cls if use_ssl else server_cls
httpd = server_class(address, SilentWSGIRequestHandler)
httpd.set_app(app)
httpd.address = httpd.server_address
server_thread = threading.Thread(
target=lambda: httpd.serve_forever(poll_interval=0.05))
server_thread.start()
try:
yield httpd
finally:
httpd.shutdown()
httpd.server_close()
server_thread.join()
if hasattr(socket, 'AF_UNIX'):
class UnixHTTPServer(socketserver.UnixStreamServer, HTTPServer):
def server_bind(self):
socketserver.UnixStreamServer.server_bind(self)
self.server_name = '127.0.0.1'
self.server_port = 80
class UnixWSGIServer(UnixHTTPServer, WSGIServer):
request_timeout = 2
def server_bind(self):
UnixHTTPServer.server_bind(self)
self.setup_environ()
def get_request(self):
request, client_addr = super().get_request()
request.settimeout(self.request_timeout)
# Code in the stdlib expects that get_request
# will return a socket and a tuple (host, port).
# However, this isn't true for UNIX sockets,
# as the second return value will be a path;
# hence we return some fake data sufficient
# to get the tests going
return request, ('127.0.0.1', '')
class SilentUnixWSGIServer(UnixWSGIServer):
def handle_error(self, request, client_address):
pass
class UnixSSLWSGIServer(SSLWSGIServerMixin, SilentUnixWSGIServer):
pass
def gen_unix_socket_path():
with tempfile.NamedTemporaryFile() as file:
return file.name
@contextlib.contextmanager
def unix_socket_path():
path = gen_unix_socket_path()
try:
yield path
finally:
try:
os.unlink(path)
except OSError:
pass
@contextlib.contextmanager
def run_test_unix_server(*, use_ssl=False):
with unix_socket_path() as path:
yield from _run_test_server(address=path, use_ssl=use_ssl,
server_cls=SilentUnixWSGIServer,
server_ssl_cls=UnixSSLWSGIServer)
@contextlib.contextmanager
def run_test_server(*, host='127.0.0.1', port=0, use_ssl=False):
yield from _run_test_server(address=(host, port), use_ssl=use_ssl,
server_cls=SilentWSGIServer,
server_ssl_cls=SSLWSGIServer)
def make_test_protocol(base):
dct = {}
for name in dir(base):
if name.startswith('__') and name.endswith('__'):
# skip magic names
continue
dct[name] = MockCallback(return_value=None)
return type('TestProtocol', (base,) + base.__bases__, dct)()
class TestSelector(selectors.BaseSelector):
def __init__(self):
self.keys = {}
def register(self, fileobj, events, data=None):
key = selectors.SelectorKey(fileobj, 0, events, data)
self.keys[fileobj] = key
return key
def unregister(self, fileobj):
return self.keys.pop(fileobj)
def select(self, timeout):
return []
def get_map(self):
return self.keys
class TestLoop(base_events.BaseEventLoop):
"""Loop for unittests.
It manages self time directly.
If something scheduled to be executed later then
on next loop iteration after all ready handlers done
generator passed to __init__ is calling.
Generator should be like this:
def gen():
...
when = yield ...
... = yield time_advance
Value returned by yield is absolute time of next scheduled handler.
Value passed to yield is time advance to move loop's time forward.
"""
def __init__(self, gen=None):
super().__init__()
if gen is None:
def gen():
yield
self._check_on_close = False
else:
self._check_on_close = True
self._gen = gen()
next(self._gen)
self._time = 0
self._clock_resolution = 1e-9
self._timers = []
self._selector = TestSelector()
self.readers = {}
self.writers = {}
self.reset_counters()
self._transports = weakref.WeakValueDictionary()
def time(self):
return self._time
def advance_time(self, advance):
"""Move test time forward."""
if advance:
self._time += advance
def close(self):
super().close()
if self._check_on_close:
try:
self._gen.send(0)
except StopIteration:
pass
else: # pragma: no cover
raise AssertionError("Time generator is not finished")
def _add_reader(self, fd, callback, *args):
self.readers[fd] = events.Handle(callback, args, self)
def _remove_reader(self, fd):
self.remove_reader_count[fd] += 1
if fd in self.readers:
del self.readers[fd]
return True
else:
return False
def assert_reader(self, fd, callback, *args):
assert fd in self.readers, 'fd {} is not registered'.format(fd)
handle = self.readers[fd]
assert handle._callback == callback, '{!r} != {!r}'.format(
handle._callback, callback)
assert handle._args == args, '{!r} != {!r}'.format(
handle._args, args)
def _add_writer(self, fd, callback, *args):
self.writers[fd] = events.Handle(callback, args, self)
def _remove_writer(self, fd):
self.remove_writer_count[fd] += 1
if fd in self.writers:
del self.writers[fd]
return True
else:
return False
def assert_writer(self, fd, callback, *args):
assert fd in self.writers, 'fd {} is not registered'.format(fd)
handle = self.writers[fd]
assert handle._callback == callback, '{!r} != {!r}'.format(
handle._callback, callback)
assert handle._args == args, '{!r} != {!r}'.format(
handle._args, args)
def _ensure_fd_no_transport(self, fd):
try:
transport = self._transports[fd]
except KeyError:
pass
else:
raise RuntimeError(
'File descriptor {!r} is used by transport {!r}'.format(
fd, transport))
def add_reader(self, fd, callback, *args):
"""Add a reader callback."""
self._ensure_fd_no_transport(fd)
return self._add_reader(fd, callback, *args)
def remove_reader(self, fd):
"""Remove a reader callback."""
self._ensure_fd_no_transport(fd)
return self._remove_reader(fd)
def add_writer(self, fd, callback, *args):
"""Add a writer callback.."""
self._ensure_fd_no_transport(fd)
return self._add_writer(fd, callback, *args)
def remove_writer(self, fd):
"""Remove a writer callback."""
self._ensure_fd_no_transport(fd)
return self._remove_writer(fd)
def reset_counters(self):
self.remove_reader_count = collections.defaultdict(int)
self.remove_writer_count = collections.defaultdict(int)
def _run_once(self):
super()._run_once()
for when in self._timers:
advance = self._gen.send(when)
self.advance_time(advance)
self._timers = []
def call_at(self, when, callback, *args):
self._timers.append(when)
return super().call_at(when, callback, *args)
def _process_events(self, event_list):
return
def _write_to_self(self):
pass
def MockCallback(**kwargs):
return mock.Mock(spec=['__call__'], **kwargs)
class MockPattern(str):
"""A regex based str with a fuzzy __eq__.
Use this helper with 'mock.assert_called_with', or anywhere
where a regex comparison between strings is needed.
For instance:
mock_call.assert_called_with(MockPattern('spam.*ham'))
"""
def __eq__(self, other):
return bool(re.search(str(self), other, re.S))
def get_function_source(func):
source = events._get_function_source(func)
if source is None:
raise ValueError("unable to get the source of %r" % (func,))
return source
class TestCase(unittest.TestCase):
def set_event_loop(self, loop, *, cleanup=True):
assert loop is not None
# ensure that the event loop is passed explicitly in asyncio
events.set_event_loop(None)
if cleanup:
self.addCleanup(loop.close)
def new_test_loop(self, gen=None):
loop = TestLoop(gen)
self.set_event_loop(loop)
return loop
def setUp(self):
self._get_running_loop = events._get_running_loop
events._get_running_loop = lambda: None
def tearDown(self):
events._get_running_loop = self._get_running_loop
events.set_event_loop(None)
# Detect CPython bug #23353: ensure that yield/yield-from is not used
# in an except block of a generator
self.assertEqual(sys.exc_info(), (None, None, None))
if not compat.PY34:
# Python 3.3 compatibility
def subTest(self, *args, **kwargs):
class EmptyCM:
def __enter__(self):
pass
def __exit__(self, *exc):
pass
return EmptyCM()
@contextlib.contextmanager
def disable_logger():
"""Context manager to disable asyncio logger.
For example, it can be used to ignore warnings in debug mode.
"""
old_level = logger.level
try:
logger.setLevel(logging.CRITICAL+1)
yield
finally:
logger.setLevel(old_level)
def mock_nonblocking_socket(proto=socket.IPPROTO_TCP, type=socket.SOCK_STREAM,
family=socket.AF_INET):
"""Create a mock of a non-blocking socket."""
sock = mock.MagicMock(socket.socket)
sock.proto = proto
sock.type = type
sock.family = family
sock.gettimeout.return_value = 0.0
return sock
def force_legacy_ssl_support():
return mock.patch('asyncio.sslproto._is_sslproto_available',
return_value=False)

25
Lib/asyncio/threads.py vendored Normal file
View File

@@ -0,0 +1,25 @@
"""High-level support for working with threads in asyncio"""
import functools
import contextvars
from . import events
__all__ = "to_thread",
async def to_thread(func, /, *args, **kwargs):
"""Asynchronously run function *func* in a separate thread.
Any *args and **kwargs supplied for this function are directly passed
to *func*. Also, the current :class:`contextvars.Context` is propagated,
allowing context variables from the main thread to be accessed in the
separate thread.
Return a coroutine that can be awaited to get the eventual result of *func*.
"""
loop = events.get_running_loop()
ctx = contextvars.copy_context()
func_call = functools.partial(ctx.run, func, *args, **kwargs)
return await loop.run_in_executor(None, func_call)

168
Lib/asyncio/timeouts.py vendored Normal file
View File

@@ -0,0 +1,168 @@
import enum
from types import TracebackType
from typing import final, Optional, Type
from . import events
from . import exceptions
from . import tasks
__all__ = (
"Timeout",
"timeout",
"timeout_at",
)
class _State(enum.Enum):
CREATED = "created"
ENTERED = "active"
EXPIRING = "expiring"
EXPIRED = "expired"
EXITED = "finished"
@final
class Timeout:
"""Asynchronous context manager for cancelling overdue coroutines.
Use `timeout()` or `timeout_at()` rather than instantiating this class directly.
"""
def __init__(self, when: Optional[float]) -> None:
"""Schedule a timeout that will trigger at a given loop time.
- If `when` is `None`, the timeout will never trigger.
- If `when < loop.time()`, the timeout will trigger on the next
iteration of the event loop.
"""
self._state = _State.CREATED
self._timeout_handler: Optional[events.TimerHandle] = None
self._task: Optional[tasks.Task] = None
self._when = when
def when(self) -> Optional[float]:
"""Return the current deadline."""
return self._when
def reschedule(self, when: Optional[float]) -> None:
"""Reschedule the timeout."""
if self._state is not _State.ENTERED:
if self._state is _State.CREATED:
raise RuntimeError("Timeout has not been entered")
raise RuntimeError(
f"Cannot change state of {self._state.value} Timeout",
)
self._when = when
if self._timeout_handler is not None:
self._timeout_handler.cancel()
if when is None:
self._timeout_handler = None
else:
loop = events.get_running_loop()
if when <= loop.time():
self._timeout_handler = loop.call_soon(self._on_timeout)
else:
self._timeout_handler = loop.call_at(when, self._on_timeout)
def expired(self) -> bool:
"""Is timeout expired during execution?"""
return self._state in (_State.EXPIRING, _State.EXPIRED)
def __repr__(self) -> str:
info = ['']
if self._state is _State.ENTERED:
when = round(self._when, 3) if self._when is not None else None
info.append(f"when={when}")
info_str = ' '.join(info)
return f"<Timeout [{self._state.value}]{info_str}>"
async def __aenter__(self) -> "Timeout":
if self._state is not _State.CREATED:
raise RuntimeError("Timeout has already been entered")
task = tasks.current_task()
if task is None:
raise RuntimeError("Timeout should be used inside a task")
self._state = _State.ENTERED
self._task = task
self._cancelling = self._task.cancelling()
self.reschedule(self._when)
return self
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> Optional[bool]:
assert self._state in (_State.ENTERED, _State.EXPIRING)
if self._timeout_handler is not None:
self._timeout_handler.cancel()
self._timeout_handler = None
if self._state is _State.EXPIRING:
self._state = _State.EXPIRED
if self._task.uncancel() <= self._cancelling and exc_type is exceptions.CancelledError:
# Since there are no new cancel requests, we're
# handling this.
raise TimeoutError from exc_val
elif self._state is _State.ENTERED:
self._state = _State.EXITED
return None
def _on_timeout(self) -> None:
assert self._state is _State.ENTERED
self._task.cancel()
self._state = _State.EXPIRING
# drop the reference early
self._timeout_handler = None
def timeout(delay: Optional[float]) -> Timeout:
"""Timeout async context manager.
Useful in cases when you want to apply timeout logic around block
of code or in cases when asyncio.wait_for is not suitable. For example:
>>> async with asyncio.timeout(10): # 10 seconds timeout
... await long_running_task()
delay - value in seconds or None to disable timeout logic
long_running_task() is interrupted by raising asyncio.CancelledError,
the top-most affected timeout() context manager converts CancelledError
into TimeoutError.
"""
loop = events.get_running_loop()
return Timeout(loop.time() + delay if delay is not None else None)
def timeout_at(when: Optional[float]) -> Timeout:
"""Schedule the timeout at absolute time.
Like timeout() but argument gives absolute time in the same clock system
as loop.time().
Please note: it is not POSIX time but a time with
undefined starting base, e.g. the time of the system power on.
>>> async with asyncio.timeout_at(loop.time() + 10):
... await long_running_task()
when - a deadline when timeout occurs or None to disable timeout logic
long_running_task() is interrupted by raising asyncio.CancelledError,
the top-most affected timeout() context manager converts CancelledError
into TimeoutError.
"""
return Timeout(when)

View File

@@ -1,15 +1,16 @@
"""Abstract Transport class."""
from asyncio import compat
__all__ = ['BaseTransport', 'ReadTransport', 'WriteTransport',
'Transport', 'DatagramTransport', 'SubprocessTransport',
]
__all__ = (
'BaseTransport', 'ReadTransport', 'WriteTransport',
'Transport', 'DatagramTransport', 'SubprocessTransport',
)
class BaseTransport:
"""Base class for transports."""
__slots__ = ('_extra',)
def __init__(self, extra=None):
if extra is None:
extra = {}
@@ -28,8 +29,8 @@ class BaseTransport:
Buffered data will be flushed asynchronously. No more data
will be received. After all buffered data is flushed, the
protocol's connection_lost() method will (eventually) called
with None as its argument.
protocol's connection_lost() method will (eventually) be
called with None as its argument.
"""
raise NotImplementedError
@@ -45,6 +46,12 @@ class BaseTransport:
class ReadTransport(BaseTransport):
"""Interface for read-only transports."""
__slots__ = ()
def is_reading(self):
"""Return True if the transport is receiving."""
raise NotImplementedError
def pause_reading(self):
"""Pause the receiving end.
@@ -65,6 +72,8 @@ class ReadTransport(BaseTransport):
class WriteTransport(BaseTransport):
"""Interface for write-only transports."""
__slots__ = ()
def set_write_buffer_limits(self, high=None, low=None):
"""Set the high- and low-water limits for write flow control.
@@ -90,6 +99,12 @@ class WriteTransport(BaseTransport):
"""Return the current size of the write buffer."""
raise NotImplementedError
def get_write_buffer_limits(self):
"""Get the high and low watermarks for write flow control.
Return a tuple (low, high) where low and high are
positive number of bytes."""
raise NotImplementedError
def write(self, data):
"""Write some data bytes to the transport.
@@ -104,7 +119,7 @@ class WriteTransport(BaseTransport):
The default implementation concatenates the arguments and
calls write() on the result.
"""
data = compat.flatten_list_bytes(list_of_data)
data = b''.join(list_of_data)
self.write(data)
def write_eof(self):
@@ -151,10 +166,14 @@ class Transport(ReadTransport, WriteTransport):
except writelines(), which calls write() in a loop.
"""
__slots__ = ()
class DatagramTransport(BaseTransport):
"""Interface for datagram (UDP) transports."""
__slots__ = ()
def sendto(self, data, addr=None):
"""Send data to the transport.
@@ -177,6 +196,8 @@ class DatagramTransport(BaseTransport):
class SubprocessTransport(BaseTransport):
__slots__ = ()
def get_pid(self):
"""Get subprocess id."""
raise NotImplementedError
@@ -244,6 +265,8 @@ class _FlowControlMixin(Transport):
resume_writing() may be called.
"""
__slots__ = ('_loop', '_protocol_paused', '_high_water', '_low_water')
def __init__(self, extra=None, loop=None):
super().__init__(extra)
assert loop is not None
@@ -259,7 +282,9 @@ class _FlowControlMixin(Transport):
self._protocol_paused = True
try:
self._protocol.pause_writing()
except Exception as exc:
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
self._loop.call_exception_handler({
'message': 'protocol.pause_writing() failed',
'exception': exc,
@@ -269,11 +294,13 @@ class _FlowControlMixin(Transport):
def _maybe_resume_protocol(self):
if (self._protocol_paused and
self.get_write_buffer_size() <= self._low_water):
self.get_write_buffer_size() <= self._low_water):
self._protocol_paused = False
try:
self._protocol.resume_writing()
except Exception as exc:
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
self._loop.call_exception_handler({
'message': 'protocol.resume_writing() failed',
'exception': exc,
@@ -287,14 +314,16 @@ class _FlowControlMixin(Transport):
def _set_write_buffer_limits(self, high=None, low=None):
if high is None:
if low is None:
high = 64*1024
high = 64 * 1024
else:
high = 4*low
high = 4 * low
if low is None:
low = high // 4
if not high >= low >= 0:
raise ValueError('high (%r) must be >= low (%r) must be >= 0' %
(high, low))
raise ValueError(
f'high ({high!r}) must be >= low ({low!r}) must be >= 0')
self._high_water = high
self._low_water = low

98
Lib/asyncio/trsock.py vendored Normal file
View File

@@ -0,0 +1,98 @@
import socket
class TransportSocket:
"""A socket-like wrapper for exposing real transport sockets.
These objects can be safely returned by APIs like
`transport.get_extra_info('socket')`. All potentially disruptive
operations (like "socket.close()") are banned.
"""
__slots__ = ('_sock',)
def __init__(self, sock: socket.socket):
self._sock = sock
@property
def family(self):
return self._sock.family
@property
def type(self):
return self._sock.type
@property
def proto(self):
return self._sock.proto
def __repr__(self):
s = (
f"<asyncio.TransportSocket fd={self.fileno()}, "
f"family={self.family!s}, type={self.type!s}, "
f"proto={self.proto}"
)
if self.fileno() != -1:
try:
laddr = self.getsockname()
if laddr:
s = f"{s}, laddr={laddr}"
except socket.error:
pass
try:
raddr = self.getpeername()
if raddr:
s = f"{s}, raddr={raddr}"
except socket.error:
pass
return f"{s}>"
def __getstate__(self):
raise TypeError("Cannot serialize asyncio.TransportSocket object")
def fileno(self):
return self._sock.fileno()
def dup(self):
return self._sock.dup()
def get_inheritable(self):
return self._sock.get_inheritable()
def shutdown(self, how):
# asyncio doesn't currently provide a high-level transport API
# to shutdown the connection.
self._sock.shutdown(how)
def getsockopt(self, *args, **kwargs):
return self._sock.getsockopt(*args, **kwargs)
def setsockopt(self, *args, **kwargs):
self._sock.setsockopt(*args, **kwargs)
def getpeername(self):
return self._sock.getpeername()
def getsockname(self):
return self._sock.getsockname()
def getsockbyname(self):
return self._sock.getsockbyname()
def settimeout(self, value):
if value == 0:
return
raise ValueError(
'settimeout(): only 0 timeout is allowed on transport sockets')
def gettimeout(self):
return 0
def setblocking(self, flag):
if not flag:
return
raise ValueError(
'setblocking(): transport sockets cannot be blocking')

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,41 @@
"""Selector and proactor event loops for Windows."""
import sys
if sys.platform != 'win32': # pragma: no cover
raise ImportError('win32 only')
import _overlapped
import _winapi
import errno
from functools import partial
import math
import msvcrt
import socket
import struct
import time
import weakref
from . import events
from . import base_subprocess
from . import futures
from . import exceptions
from . import proactor_events
from . import selector_events
from . import tasks
from . import windows_utils
# XXX RustPython TODO: _overlapped
# from . import _overlapped
from .coroutines import coroutine
from .log import logger
__all__ = ['SelectorEventLoop', 'ProactorEventLoop', 'IocpProactor',
'DefaultEventLoopPolicy',
]
__all__ = (
'SelectorEventLoop', 'ProactorEventLoop', 'IocpProactor',
'DefaultEventLoopPolicy', 'WindowsSelectorEventLoopPolicy',
'WindowsProactorEventLoopPolicy',
)
NULL = 0
INFINITE = 0xffffffff
NULL = _winapi.NULL
INFINITE = _winapi.INFINITE
ERROR_CONNECTION_REFUSED = 1225
ERROR_CONNECTION_ABORTED = 1236
@@ -53,7 +62,7 @@ class _OverlappedFuture(futures.Future):
info = super()._repr_info()
if self._ov is not None:
state = 'pending' if self._ov.pending else 'completed'
info.insert(1, 'overlapped=<%s, %#x>' % (state, self._ov.address))
info.insert(1, f'overlapped=<{state}, {self._ov.address:#x}>')
return info
def _cancel_overlapped(self):
@@ -72,9 +81,9 @@ class _OverlappedFuture(futures.Future):
self._loop.call_exception_handler(context)
self._ov = None
def cancel(self):
def cancel(self, msg=None):
self._cancel_overlapped()
return super().cancel()
return super().cancel(msg=msg)
def set_exception(self, exception):
super().set_exception(exception)
@@ -109,12 +118,12 @@ class _BaseWaitHandleFuture(futures.Future):
def _repr_info(self):
info = super()._repr_info()
info.append('handle=%#x' % self._handle)
info.append(f'handle={self._handle:#x}')
if self._handle is not None:
state = 'signaled' if self._poll() else 'waiting'
info.append(state)
if self._wait_handle is not None:
info.append('wait_handle=%#x' % self._wait_handle)
info.append(f'wait_handle={self._wait_handle:#x}')
return info
def _unregister_wait_cb(self, fut):
@@ -146,9 +155,9 @@ class _BaseWaitHandleFuture(futures.Future):
self._unregister_wait_cb(None)
def cancel(self):
def cancel(self, msg=None):
self._unregister_wait()
return super().cancel()
return super().cancel(msg=msg)
def set_exception(self, exception):
self._unregister_wait()
@@ -297,9 +306,6 @@ class PipeServer(object):
class _WindowsSelectorEventLoop(selector_events.BaseSelectorEventLoop):
"""Windows version of selector event loop."""
def _socketpair(self):
return windows_utils.socketpair()
class ProactorEventLoop(proactor_events.BaseProactorEventLoop):
"""Windows version of proactor event loop using IOCP."""
@@ -309,20 +315,34 @@ class ProactorEventLoop(proactor_events.BaseProactorEventLoop):
proactor = IocpProactor()
super().__init__(proactor)
def _socketpair(self):
return windows_utils.socketpair()
def run_forever(self):
try:
assert self._self_reading_future is None
self.call_soon(self._loop_self_reading)
super().run_forever()
finally:
if self._self_reading_future is not None:
ov = self._self_reading_future._ov
self._self_reading_future.cancel()
# self_reading_future always uses IOCP, so even though it's
# been cancelled, we need to make sure that the IOCP message
# is received so that the kernel is not holding on to the
# memory, possibly causing memory corruption later. Only
# unregister it if IO is complete in all respects. Otherwise
# we need another _poll() later to complete the IO.
if ov is not None and not ov.pending:
self._proactor._unregister(ov)
self._self_reading_future = None
@coroutine
def create_pipe_connection(self, protocol_factory, address):
async def create_pipe_connection(self, protocol_factory, address):
f = self._proactor.connect_pipe(address)
pipe = yield from f
pipe = await f
protocol = protocol_factory()
trans = self._make_duplex_pipe_transport(pipe, protocol,
extra={'addr': address})
return trans, protocol
@coroutine
def start_serving_pipe(self, protocol_factory, address):
async def start_serving_pipe(self, protocol_factory, address):
server = PipeServer(address)
def loop_accept_pipe(f=None):
@@ -347,6 +367,10 @@ class ProactorEventLoop(proactor_events.BaseProactorEventLoop):
return
f = self._proactor.accept_pipe(pipe)
except BrokenPipeError:
if pipe and pipe.fileno() != -1:
pipe.close()
self.call_soon(loop_accept_pipe)
except OSError as exc:
if pipe and pipe.fileno() != -1:
self.call_exception_handler({
@@ -358,7 +382,8 @@ class ProactorEventLoop(proactor_events.BaseProactorEventLoop):
elif self._debug:
logger.warning("Accept pipe failed on pipe %r",
pipe, exc_info=True)
except futures.CancelledError:
self.call_soon(loop_accept_pipe)
except exceptions.CancelledError:
if pipe:
pipe.close()
else:
@@ -368,28 +393,22 @@ class ProactorEventLoop(proactor_events.BaseProactorEventLoop):
self.call_soon(loop_accept_pipe)
return [server]
@coroutine
def _make_subprocess_transport(self, protocol, args, shell,
stdin, stdout, stderr, bufsize,
extra=None, **kwargs):
async def _make_subprocess_transport(self, protocol, args, shell,
stdin, stdout, stderr, bufsize,
extra=None, **kwargs):
waiter = self.create_future()
transp = _WindowsSubprocessTransport(self, protocol, args, shell,
stdin, stdout, stderr, bufsize,
waiter=waiter, extra=extra,
**kwargs)
try:
yield from waiter
except Exception as exc:
# Workaround CPython bug #23353: using yield/yield-from in an
# except block of a generator doesn't clear properly sys.exc_info()
err = exc
else:
err = None
if err is not None:
await waiter
except (SystemExit, KeyboardInterrupt):
raise
except BaseException:
transp.close()
yield from transp._wait()
raise err
await transp._wait()
raise
return transp
@@ -397,7 +416,7 @@ class ProactorEventLoop(proactor_events.BaseProactorEventLoop):
class IocpProactor:
"""Proactor implementation using IOCP."""
def __init__(self, concurrency=0xffffffff):
def __init__(self, concurrency=INFINITE):
self._loop = None
self._results = []
self._iocp = _overlapped.CreateIoCompletionPort(
@@ -407,10 +426,16 @@ class IocpProactor:
self._unregistered = []
self._stopped_serving = weakref.WeakSet()
def _check_closed(self):
if self._iocp is None:
raise RuntimeError('IocpProactor is closed')
def __repr__(self):
return ('<%s overlapped#=%s result#=%s>'
% (self.__class__.__name__, len(self._cache),
len(self._results)))
info = ['overlapped#=%s' % len(self._cache),
'result#=%s' % len(self._results)]
if self._iocp is None:
info.append('closed')
return '<%s %s>' % (self.__class__.__name__, " ".join(info))
def set_loop(self, loop):
self._loop = loop
@@ -420,13 +445,40 @@ class IocpProactor:
self._poll(timeout)
tmp = self._results
self._results = []
return tmp
try:
return tmp
finally:
# Needed to break cycles when an exception occurs.
tmp = None
def _result(self, value):
fut = self._loop.create_future()
fut.set_result(value)
return fut
@staticmethod
def finish_socket_func(trans, key, ov):
try:
return ov.getresult()
except OSError as exc:
if exc.winerror in (_overlapped.ERROR_NETNAME_DELETED,
_overlapped.ERROR_OPERATION_ABORTED):
raise ConnectionResetError(*exc.args)
else:
raise
@classmethod
def _finish_recvfrom(cls, trans, key, ov, *, empty_result):
try:
return cls.finish_socket_func(trans, key, ov)
except OSError as exc:
# WSARecvFrom will report ERROR_PORT_UNREACHABLE when the same
# socket is used to send to an address that is not listening.
if exc.winerror == _overlapped.ERROR_PORT_UNREACHABLE:
return empty_result, None
else:
raise
def recv(self, conn, nbytes, flags=0):
self._register_with_iocp(conn)
ov = _overlapped.Overlapped(NULL)
@@ -438,16 +490,50 @@ class IocpProactor:
except BrokenPipeError:
return self._result(b'')
def finish_recv(trans, key, ov):
try:
return ov.getresult()
except OSError as exc:
if exc.winerror == _overlapped.ERROR_NETNAME_DELETED:
raise ConnectionResetError(*exc.args)
else:
raise
return self._register(ov, conn, self.finish_socket_func)
return self._register(ov, conn, finish_recv)
def recv_into(self, conn, buf, flags=0):
self._register_with_iocp(conn)
ov = _overlapped.Overlapped(NULL)
try:
if isinstance(conn, socket.socket):
ov.WSARecvInto(conn.fileno(), buf, flags)
else:
ov.ReadFileInto(conn.fileno(), buf)
except BrokenPipeError:
return self._result(0)
return self._register(ov, conn, self.finish_socket_func)
def recvfrom(self, conn, nbytes, flags=0):
self._register_with_iocp(conn)
ov = _overlapped.Overlapped(NULL)
try:
ov.WSARecvFrom(conn.fileno(), nbytes, flags)
except BrokenPipeError:
return self._result((b'', None))
return self._register(ov, conn, partial(self._finish_recvfrom,
empty_result=b''))
def recvfrom_into(self, conn, buf, flags=0):
self._register_with_iocp(conn)
ov = _overlapped.Overlapped(NULL)
try:
ov.WSARecvFromInto(conn.fileno(), buf, flags)
except BrokenPipeError:
return self._result((0, None))
return self._register(ov, conn, partial(self._finish_recvfrom,
empty_result=0))
def sendto(self, conn, buf, flags=0, addr=None):
self._register_with_iocp(conn)
ov = _overlapped.Overlapped(NULL)
ov.WSASendTo(conn.fileno(), buf, flags, addr)
return self._register(ov, conn, self.finish_socket_func)
def send(self, conn, buf, flags=0):
self._register_with_iocp(conn)
@@ -457,16 +543,7 @@ class IocpProactor:
else:
ov.WriteFile(conn.fileno(), buf)
def finish_send(trans, key, ov):
try:
return ov.getresult()
except OSError as exc:
if exc.winerror == _overlapped.ERROR_NETNAME_DELETED:
raise ConnectionResetError(*exc.args)
else:
raise
return self._register(ov, conn, finish_send)
return self._register(ov, conn, self.finish_socket_func)
def accept(self, listener):
self._register_with_iocp(listener)
@@ -483,12 +560,11 @@ class IocpProactor:
conn.settimeout(listener.gettimeout())
return conn, conn.getpeername()
@coroutine
def accept_coro(future, conn):
async def accept_coro(future, conn):
# Coroutine closing the accept socket if the future is cancelled
try:
yield from future
except futures.CancelledError:
await future
except exceptions.CancelledError:
conn.close()
raise
@@ -498,6 +574,14 @@ class IocpProactor:
return future
def connect(self, conn, address):
if conn.type == socket.SOCK_DGRAM:
# WSAConnect will complete immediately for UDP sockets so we don't
# need to register any IOCP operation
_overlapped.WSAConnect(conn.fileno(), address)
fut = self._loop.create_future()
fut.set_result(None)
return fut
self._register_with_iocp(conn)
# The socket needs to be locally bound before we call ConnectEx().
try:
@@ -520,6 +604,18 @@ class IocpProactor:
return self._register(ov, conn, finish_connect)
def sendfile(self, sock, file, offset, count):
self._register_with_iocp(sock)
ov = _overlapped.Overlapped(NULL)
offset_low = offset & 0xffff_ffff
offset_high = (offset >> 32) & 0xffff_ffff
ov.TransmitFile(sock.fileno(),
msvcrt.get_osfhandle(file.fileno()),
offset_low, offset_high,
count, 0, 0)
return self._register(ov, sock, self.finish_socket_func)
def accept_pipe(self, pipe):
self._register_with_iocp(pipe)
ov = _overlapped.Overlapped(NULL)
@@ -537,13 +633,12 @@ class IocpProactor:
return self._register(ov, pipe, finish_accept_pipe)
@coroutine
def connect_pipe(self, address):
async def connect_pipe(self, address):
delay = CONNECT_PIPE_INIT_DELAY
while True:
# Unfortunately there is no way to do an overlapped connect to a pipe.
# Call CreateFile() in a loop until it doesn't fail with
# ERROR_PIPE_BUSY
# Unfortunately there is no way to do an overlapped connect to
# a pipe. Call CreateFile() in a loop until it doesn't fail with
# ERROR_PIPE_BUSY.
try:
handle = _overlapped.ConnectPipe(address)
break
@@ -553,7 +648,7 @@ class IocpProactor:
# ConnectPipe() failed with ERROR_PIPE_BUSY: retry later
delay = min(delay * 2, CONNECT_PIPE_MAX_DELAY)
yield from tasks.sleep(delay, loop=self._loop)
await tasks.sleep(delay)
return windows_utils.PipeHandle(handle)
@@ -573,6 +668,8 @@ class IocpProactor:
return fut
def _wait_for_handle(self, handle, timeout, _is_cancel):
self._check_closed()
if timeout is None:
ms = _winapi.INFINITE
else:
@@ -615,6 +712,8 @@ class IocpProactor:
# that succeed immediately.
def _register(self, ov, obj, callback):
self._check_closed()
# Return a future which will be set with the result of the
# operation when it completes. The future's value is actually
# the value returned by callback().
@@ -651,6 +750,7 @@ class IocpProactor:
already be signalled (pending in the proactor event queue). It is also
safe if the event is never signalled (because it was cancelled).
"""
self._check_closed()
self._unregistered.append(ov)
def _get_accept_socket(self, family):
@@ -707,8 +807,10 @@ class IocpProactor:
else:
f.set_result(value)
self._results.append(f)
finally:
f = None
# Remove unregisted futures
# Remove unregistered futures
for ov in self._unregistered:
self._cache.pop(ov.address, None)
self._unregistered.clear()
@@ -720,8 +822,12 @@ class IocpProactor:
self._stopped_serving.add(obj)
def close(self):
if self._iocp is None:
# already closed
return
# Cancel remaining registered operations.
for address, (fut, ov, obj, callback) in list(self._cache.items()):
for fut, ov, obj, callback in list(self._cache.values()):
if fut.cancelled():
# Nothing to do with cancelled futures
pass
@@ -742,14 +848,25 @@ class IocpProactor:
context['source_traceback'] = fut._source_traceback
self._loop.call_exception_handler(context)
# Wait until all cancelled overlapped complete: don't exit with running
# overlapped to prevent a crash. Display progress every second if the
# loop is still running.
msg_update = 1.0
start_time = time.monotonic()
next_msg = start_time + msg_update
while self._cache:
if not self._poll(1):
logger.debug('taking long time to close proactor')
if next_msg <= time.monotonic():
logger.debug('%r is running after closing for %.1f seconds',
self, time.monotonic() - start_time)
next_msg = time.monotonic() + msg_update
# handle a few events, or timeout
self._poll(msg_update)
self._results = []
if self._iocp is not None:
_winapi.CloseHandle(self._iocp)
self._iocp = None
_winapi.CloseHandle(self._iocp)
self._iocp = None
def __del__(self):
self.close()
@@ -773,8 +890,12 @@ class _WindowsSubprocessTransport(base_subprocess.BaseSubprocessTransport):
SelectorEventLoop = _WindowsSelectorEventLoop
class _WindowsDefaultEventLoopPolicy(events.BaseDefaultEventLoopPolicy):
class WindowsSelectorEventLoopPolicy(events.BaseDefaultEventLoopPolicy):
_loop_factory = SelectorEventLoop
DefaultEventLoopPolicy = _WindowsDefaultEventLoopPolicy
class WindowsProactorEventLoopPolicy(events.BaseDefaultEventLoopPolicy):
_loop_factory = ProactorEventLoop
DefaultEventLoopPolicy = WindowsProactorEventLoopPolicy

Some files were not shown because too many files have changed in this diff Show More