mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Compare commits
1 Commits
2025-07-28
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5343c0bd0b |
@@ -1,5 +0,0 @@
|
||||
[target.'cfg(target_env = "msvc")']
|
||||
rustflags = "-C link-arg=/STACK:8000000"
|
||||
|
||||
[target.'cfg(all(target_os = "windows", not(target_env = "msvc")))']
|
||||
rustflags = "-C link-args=-Wl,--stack,8000000"
|
||||
@@ -1,3 +0,0 @@
|
||||
reviews:
|
||||
path_filters:
|
||||
- "!Lib/**"
|
||||
@@ -1,61 +0,0 @@
|
||||
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
|
||||
@@ -1,265 +0,0 @@
|
||||
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
|
||||
@@ -1,86 +0,0 @@
|
||||
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
|
||||
153
.cspell.json
153
.cspell.json
@@ -1,153 +0,0 @@
|
||||
// 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",
|
||||
"rust",
|
||||
"shell",
|
||||
"win32"
|
||||
],
|
||||
// dictionaryDefinitions - this list defines any custom dictionaries to use
|
||||
"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": [
|
||||
"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",
|
||||
"PyClassMethod",
|
||||
"PyException",
|
||||
"PyFunction",
|
||||
"pygetset",
|
||||
"pyimpl",
|
||||
"pylib",
|
||||
"pymath",
|
||||
"pymember",
|
||||
"PyMethod",
|
||||
"PyModule",
|
||||
"pyname",
|
||||
"pyobj",
|
||||
"PyObject",
|
||||
"pypayload",
|
||||
"PyProperty",
|
||||
"pyref",
|
||||
"PyResult",
|
||||
"pyslot",
|
||||
"PyStaticMethod",
|
||||
"pystone",
|
||||
"pystr",
|
||||
"pystruct",
|
||||
"pystructseq",
|
||||
"pytrace",
|
||||
"reducelib",
|
||||
"richcompare",
|
||||
"RustPython",
|
||||
"significand",
|
||||
"struc",
|
||||
"summands", // plural of summand
|
||||
"sysmodule",
|
||||
"tracebacks",
|
||||
"typealiases",
|
||||
"unconstructible",
|
||||
"unhashable",
|
||||
"uninit",
|
||||
"unraisable",
|
||||
"unresizable",
|
||||
"wasi",
|
||||
"zelf",
|
||||
// unix
|
||||
"CLOEXEC",
|
||||
"codeset",
|
||||
"endgrent",
|
||||
"gethrvtime",
|
||||
"getrusage",
|
||||
"nanosleep",
|
||||
"sigaction",
|
||||
"WRLCK",
|
||||
// win32
|
||||
"birthtime",
|
||||
"IFEXEC",
|
||||
// "stat"
|
||||
"FIRMLINK"
|
||||
],
|
||||
// flagWords - list of words to be always considered incorrect
|
||||
"flagWords": [
|
||||
],
|
||||
"ignoreRegExpList": [
|
||||
],
|
||||
// languageSettings - allow for per programming language configuration settings.
|
||||
"languageSettings": [
|
||||
{
|
||||
"languageId": "python",
|
||||
"locale": "en"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/rust:1-bullseye
|
||||
|
||||
# Install clang
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y clang \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
.vscode
|
||||
wasm-pack.log
|
||||
.idea/
|
||||
extra_tests/snippets/resources
|
||||
tests/snippets/resources
|
||||
|
||||
flame-graph.html
|
||||
flame.txt
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
ignore_patterns:
|
||||
- "Lib/**"
|
||||
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,7 +1 @@
|
||||
Lib/** linguist-vendored
|
||||
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
|
||||
Lib/* linguist-vendored
|
||||
|
||||
16
.github/ISSUE_TEMPLATE/empty.md
vendored
16
.github/ISSUE_TEMPLATE/empty.md
vendored
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Generic issue template
|
||||
about: which is not covered by other templates
|
||||
title: ''
|
||||
labels:
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Short description of the issue. -->
|
||||
|
||||
## Details
|
||||
|
||||
<!-- Whatever you want to share -->
|
||||
16
.github/ISSUE_TEMPLATE/feature-request.md
vendored
16
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a feature to use RustPython (as a Rust library)
|
||||
title: ''
|
||||
labels: C-enhancement
|
||||
assignees: 'youknowone'
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Short description of the request. Please use incompatibility form to report missing features as Python interpreter -->
|
||||
|
||||
## Expected use case
|
||||
|
||||
<!-- By sharing detailed use case, we can understand the requirements better! If it will be used by open source projects, please also share the project URL. -->
|
||||
24
.github/ISSUE_TEMPLATE/report-bug.md
vendored
24
.github/ISSUE_TEMPLATE/report-bug.md
vendored
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: Report bugs
|
||||
about: Report a bug not related to CPython compatibility
|
||||
title: ''
|
||||
labels: C-bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
<!-- Short description of the bug -->
|
||||
|
||||
## Expected
|
||||
|
||||
<!-- What's the expected result? Using ``` ``` block is preferred for text. -->
|
||||
|
||||
## Actual
|
||||
|
||||
<!-- What's the actual result? Using ``` ``` block is preferred for text. -->
|
||||
|
||||
## Python Documentation
|
||||
|
||||
<!-- If applicable. -->
|
||||
@@ -2,7 +2,7 @@
|
||||
name: Report incompatibility
|
||||
about: Report an incompatibility between RustPython and CPython
|
||||
title: ''
|
||||
labels: C-compat
|
||||
labels: feat
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -11,6 +11,6 @@ assignees: ''
|
||||
|
||||
<!-- What Python feature is missing from RustPython? Give a short description of the feature and how you ran into its absence. -->
|
||||
|
||||
## Python Documentation or reference to CPython source code
|
||||
## Python Documentation
|
||||
|
||||
<!-- Give a link to the feature in the CPython documentation (https://docs.python.org/3/) in order to assist in its implementation. -->
|
||||
|
||||
212
.github/copilot-instructions.md
vendored
212
.github/copilot-instructions.md
vendored
@@ -1,212 +0,0 @@
|
||||
# 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
13
.github/dependabot.yml
vendored
@@ -1,13 +0,0 @@
|
||||
# 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
|
||||
467
.github/workflows/ci.yaml
vendored
467
.github/workflows/ci.yaml
vendored
@@ -1,452 +1,161 @@
|
||||
on:
|
||||
push:
|
||||
branches: [main, release]
|
||||
pull_request:
|
||||
types: [unlabeled, opened, synchronize, reopened]
|
||||
merge_group:
|
||||
workflow_dispatch:
|
||||
branches: [master, release]
|
||||
pull_request:
|
||||
|
||||
name: CI
|
||||
|
||||
# Cancel previous workflows if they are the same workflow on same ref (branch/tags)
|
||||
# with the same event (push/pull_request) even they are in progress.
|
||||
# This setting will help reduce the number of duplicated workflows.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl
|
||||
# Skip additional tests on Windows. They are checked on Linux and MacOS.
|
||||
# test_glob: many failing tests
|
||||
# test_io: many failing tests
|
||||
# test_os: many failing tests
|
||||
# test_pathlib: support.rmtree() failing
|
||||
# test_posixpath: OSError: (22, 'The filename, directory name, or volume label syntax is incorrect. (os error 123)')
|
||||
# test_venv: couple of failing tests
|
||||
WINDOWS_SKIPS: >-
|
||||
test_glob
|
||||
test_io
|
||||
test_os
|
||||
test_rlcompleter
|
||||
test_pathlib
|
||||
test_posixpath
|
||||
test_venv
|
||||
# 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__colorize
|
||||
test_array
|
||||
test_asyncgen
|
||||
test_binop
|
||||
test_bisect
|
||||
test_bool
|
||||
test_bytes
|
||||
test_call
|
||||
test_class
|
||||
test_cmath
|
||||
test_collections
|
||||
test_complex
|
||||
test_contains
|
||||
test_copy
|
||||
test_dataclasses
|
||||
test_decimal
|
||||
test_decorators
|
||||
test_defaultdict
|
||||
test_deque
|
||||
test_dict
|
||||
test_dictcomps
|
||||
test_dictviews
|
||||
test_dis
|
||||
test_enumerate
|
||||
test_exception_variations
|
||||
test_float
|
||||
test_format
|
||||
test_fractions
|
||||
test_genericalias
|
||||
test_genericclass
|
||||
test_grammar
|
||||
test_range
|
||||
test_index
|
||||
test_int
|
||||
test_int_literal
|
||||
test_isinstance
|
||||
test_iter
|
||||
test_iterlen
|
||||
test_itertools
|
||||
test_json
|
||||
test_keyword
|
||||
test_keywordonlyarg
|
||||
test_list
|
||||
test_long
|
||||
test_longexp
|
||||
test_math
|
||||
test_operator
|
||||
test_ordered_dict
|
||||
test_pow
|
||||
test_raise
|
||||
test_richcmp
|
||||
test_scope
|
||||
test_set
|
||||
test_slice
|
||||
test_sort
|
||||
test_string
|
||||
test_string_literals
|
||||
test_strtod
|
||||
test_structseq
|
||||
test_subclassinit
|
||||
test_super
|
||||
test_syntax
|
||||
test_tuple
|
||||
test_types
|
||||
test_unary
|
||||
test_unpack
|
||||
test_weakref
|
||||
test_yield_from
|
||||
# Python version targeted by the CI.
|
||||
PYTHON_VERSION: "3.13.1"
|
||||
CARGO_ARGS: --all --features ssl
|
||||
|
||||
jobs:
|
||||
rust_tests:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
env:
|
||||
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@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- uses: actions/checkout@master
|
||||
- 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
|
||||
powershell.exe scripts/symlinks-to-hardlinks.ps1
|
||||
if: runner.os == 'Windows'
|
||||
- name: Set up the Mac environment
|
||||
run: brew install autoconf automake libtool
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: run clippy
|
||||
run: cargo clippy ${{ env.CARGO_ARGS }} --workspace --all-targets --exclude rustpython_wasm -- -Dwarnings
|
||||
|
||||
- name: Cache cargo dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
key: ${{ runner.os }}-rust_tests-${{ hashFiles('Cargo.lock') }}
|
||||
path: target
|
||||
restore-keys: |
|
||||
${{ runner.os }}-rust_tests-
|
||||
- name: run rust tests
|
||||
run: cargo test --workspace --exclude rustpython_wasm --verbose --features threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os != 'macOS'
|
||||
- name: run rust tests
|
||||
run: cargo test --workspace --exclude rustpython_wasm --exclude rustpython-jit --verbose --features threading ${{ env.CARGO_ARGS }}
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
- name: check compilation without threading
|
||||
run: cargo check ${{ env.CARGO_ARGS }}
|
||||
|
||||
- name: Test example projects
|
||||
run:
|
||||
cargo run --manifest-path example_projects/barebone/Cargo.toml
|
||||
cargo run --manifest-path example_projects/frozen_stdlib/Cargo.toml
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: prepare AppleSilicon build
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
target: aarch64-apple-darwin
|
||||
if: runner.os == 'macOS'
|
||||
- name: Check compilation for Apple Silicon
|
||||
run: cargo check --target aarch64-apple-darwin
|
||||
if: runner.os == 'macOS'
|
||||
- name: prepare iOS build
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: aarch64-apple-ios
|
||||
if: runner.os == 'macOS'
|
||||
- name: Check compilation for iOS
|
||||
run: cargo check --target aarch64-apple-ios
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
exotic_targets:
|
||||
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@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: i686-unknown-linux-gnu
|
||||
|
||||
- name: Install gcc-multilib and musl-tools
|
||||
run: sudo apt-get update && sudo apt-get install gcc-multilib musl-tools
|
||||
- name: Check compilation for x86 32bit
|
||||
run: cargo check --target i686-unknown-linux-gnu
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: aarch64-linux-android
|
||||
|
||||
- name: Check compilation for android
|
||||
run: cargo check --target aarch64-linux-android
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: Install gcc-aarch64-linux-gnu
|
||||
run: sudo apt install gcc-aarch64-linux-gnu
|
||||
- name: Check compilation for aarch64 linux gnu
|
||||
run: cargo check --target aarch64-unknown-linux-gnu
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: i686-unknown-linux-musl
|
||||
|
||||
- name: Check compilation for musl
|
||||
run: cargo check --target i686-unknown-linux-musl
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: x86_64-unknown-freebsd
|
||||
|
||||
- name: Check compilation for freebsd
|
||||
run: cargo check --target x86_64-unknown-freebsd
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: x86_64-unknown-freebsd
|
||||
|
||||
- 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
|
||||
# args: --ignore-rust-version
|
||||
command: test
|
||||
args: --verbose ${{ env.CARGO_ARGS }}
|
||||
|
||||
snippets_cpython:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
env:
|
||||
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@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- uses: actions/checkout@master
|
||||
- name: Set up the Windows environment
|
||||
shell: bash
|
||||
run: |
|
||||
git config --system core.longpaths true
|
||||
cargo install cargo-vcpkg
|
||||
cargo vcpkg build
|
||||
powershell.exe scripts/symlinks-to-hardlinks.ps1
|
||||
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 }}
|
||||
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
|
||||
- name: Cache cargo dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
key: ${{ runner.os }}-snippets-${{ hashFiles('Cargo.lock') }}
|
||||
path: target
|
||||
restore-keys: |
|
||||
${{ runner.os }}-snippets-
|
||||
- name: build rustpython
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --verbose ${{ env.CARGO_ARGS }}
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install pipenv
|
||||
run: |
|
||||
python -V
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pipenv
|
||||
- run: pipenv install
|
||||
working-directory: ./tests
|
||||
- name: run snippets
|
||||
run: python -m pip install -r requirements.txt && pytest -v
|
||||
working-directory: ./extra_tests
|
||||
- if: runner.os == 'Linux'
|
||||
name: run cpython platform-independent tests
|
||||
run:
|
||||
target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
- if: runner.os == 'Linux'
|
||||
name: run cpython platform-dependent tests (Linux)
|
||||
run: target/release/rustpython -m test -j 1 -u all --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
- if: runner.os == 'macOS'
|
||||
name: run cpython platform-dependent tests (MacOS)
|
||||
run: target/release/rustpython -m test -j 1 --slowest --fail-env-changed -v -x ${{ env.PLATFORM_INDEPENDENT_TESTS }}
|
||||
- if: runner.os == 'Windows'
|
||||
name: run cpython platform-dependent tests (windows partial - fixme)
|
||||
run:
|
||||
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: |
|
||||
target/release/rustpython -m ensurepip
|
||||
target/release/rustpython -c "import pip"
|
||||
- if: runner.os != 'Windows'
|
||||
name: Check if pip inside venv is functional
|
||||
run: |
|
||||
target/release/rustpython -m venv testvenv
|
||||
testvenv/bin/rustpython -m pip install wheel
|
||||
- name: Check whats_left is not broken
|
||||
run: python -I whats_left.py
|
||||
run: pipenv run pytest -v
|
||||
working-directory: ./tests
|
||||
- name: run cpython tests
|
||||
run: target/release/rustpython -m test -v
|
||||
env:
|
||||
RUSTPYTHONPATH: ${{ github.workspace }}/Lib
|
||||
if: runner.os != 'Windows'
|
||||
|
||||
lint:
|
||||
format:
|
||||
name: Check Rust code with rustfmt and clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
override: true
|
||||
- name: run rustfmt
|
||||
run: cargo fmt --check
|
||||
- name: run clippy on wasm
|
||||
run: cargo clippy --manifest-path=wasm/lib/Cargo.toml -- -Dwarnings
|
||||
- uses: actions/setup-python@v5
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: install ruff
|
||||
run: python -m pip install ruff==0.11.8
|
||||
- name: Ensure docs generate no warnings
|
||||
run: cargo doc
|
||||
- name: run ruff check
|
||||
run: ruff check --diff
|
||||
- name: run ruff format
|
||||
run: ruff format --check
|
||||
- name: install prettier
|
||||
run: yarn global add prettier && echo "$(yarn global bin)" >>$GITHUB_PATH
|
||||
- name: check wasm code with prettier
|
||||
# prettier doesn't handle ignore files very well: https://github.com/prettier/prettier/issues/8506
|
||||
run: cd wasm && git ls-files -z | xargs -0 prettier --check -u
|
||||
# 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
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- name: run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
files: '**/*.rs'
|
||||
incremental_files_only: true
|
||||
command: clippy
|
||||
args: ${{ env.CARGO_ARGS }} -- -Dwarnings
|
||||
|
||||
miri:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
|
||||
name: Run tests under miri
|
||||
lint:
|
||||
name: Lint Python code with flake8
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
NIGHTLY_CHANNEL: nightly
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
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
|
||||
MIRIFLAGS: '-Zmiri-ignore-leaks'
|
||||
python-version: 3.8
|
||||
- name: install flake8
|
||||
run: python -m pip install flake8
|
||||
- name: run lint
|
||||
run: flake8 . --count --exclude=./.*,./Lib,./vm/Lib --select=E9,F63,F7,F82 --show-source --statistics
|
||||
|
||||
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@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: actions/checkout@master
|
||||
- name: Cache cargo dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
key: ${{ runner.os }}-wasm-${{ hashFiles('**/Cargo.lock') }}
|
||||
path: target
|
||||
restore-keys: |
|
||||
${{ runner.os }}-wasm-
|
||||
- name: install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
- name: install geckodriver
|
||||
run: |
|
||||
wget https://github.com/mozilla/geckodriver/releases/download/v0.36.0/geckodriver-v0.36.0-linux64.tar.gz
|
||||
wget https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux32.tar.gz
|
||||
mkdir geckodriver
|
||||
tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver
|
||||
- uses: actions/setup-python@v5
|
||||
tar -xzf geckodriver-v0.24.0-linux32.tar.gz -C geckodriver
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- run: python -m pip install -r requirements.txt
|
||||
python-version: 3.8
|
||||
- name: Install pipenv
|
||||
run: |
|
||||
python -V
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pipenv
|
||||
- run: pipenv install
|
||||
working-directory: ./wasm/tests
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "wasm/demo/package-lock.json"
|
||||
- uses: actions/setup-node@v1
|
||||
- name: run test
|
||||
run: |
|
||||
export PATH=$PATH:`pwd`/../../geckodriver
|
||||
npm install
|
||||
npm run test
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/demo
|
||||
- uses: mwilliamson/setup-wabt-action@v3
|
||||
with: { wabt-version: "1.0.36" }
|
||||
- name: check wasm32-unknown without js
|
||||
run: |
|
||||
cd wasm/wasm-unknown-test
|
||||
cargo build --release --verbose
|
||||
if wasm-objdump -xj Import target/wasm32-unknown-unknown/release/wasm_unknown_test.wasm; then
|
||||
echo "ERROR: wasm32-unknown module expects imports from the host environment" >2
|
||||
fi
|
||||
- name: build notebook demo
|
||||
if: github.ref == 'refs/heads/release'
|
||||
run: |
|
||||
npm install
|
||||
npm run dist
|
||||
mv dist ../demo/dist/notebook
|
||||
env:
|
||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
||||
working-directory: ./wasm/notebook
|
||||
- name: Deploy demo to Github Pages
|
||||
if: success() && github.ref == 'refs/heads/release'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
uses: peaceiris/actions-gh-pages@v2
|
||||
env:
|
||||
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEMO_DEPLOY_KEY }}
|
||||
PUBLISH_DIR: ./wasm/demo/dist
|
||||
EXTERNAL_REPOSITORY: RustPython/demo
|
||||
PUBLISH_BRANCH: master
|
||||
|
||||
wasm-wasi:
|
||||
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@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: wasm32-wasip1
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Setup Wasmer
|
||||
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-wasip1 --features freeze-stdlib,stdlib --verbose
|
||||
- name: run snippets
|
||||
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
21
.github/workflows/comment-commands.yml
vendored
@@ -1,21 +0,0 @@
|
||||
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
|
||||
178
.github/workflows/cron-ci.yaml
vendored
178
.github/workflows/cron-ci.yaml
vendored
@@ -1,56 +1,74 @@
|
||||
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,importlib,encodings,ssl,jit
|
||||
PYTHON_VERSION: "3.13.1"
|
||||
|
||||
jobs:
|
||||
# codecov collects code coverage data from the rust tests, python snippets and python test suite.
|
||||
# This is done using cargo-llvm-cov, which is a wrapper around llvm-cov.
|
||||
redox:
|
||||
name: Check compilation on Redox
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: redoxos/redoxer:latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: prepare repository for redoxer compilation
|
||||
run: bash scripts/redox/uncomment-cargo.sh
|
||||
- name: compile for redox
|
||||
run: redoxer build --verbose
|
||||
|
||||
codecov:
|
||||
name: Collect code coverage data
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions-rs/toolchain@v1
|
||||
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,importlib,encodings,ssl,jit
|
||||
- name: Run cargo-llvm-cov with Python snippets.
|
||||
run: python scripts/cargo-llvm-cov.py
|
||||
continue-on-error: true
|
||||
- name: Run cargo-llvm-cov with Python test suite.
|
||||
run: cargo llvm-cov --no-report run -- -m test -u all --slowest --fail-env-changed
|
||||
continue-on-error: true
|
||||
- name: Prepare code coverage data
|
||||
run: cargo llvm-cov report --lcov --output-path='codecov.lcov'
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
file: ./codecov.lcov
|
||||
command: build
|
||||
args: --verbose
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests' # -Cpanic=abort
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install pipenv
|
||||
run: |
|
||||
python -V
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pipenv
|
||||
- run: pipenv install
|
||||
working-directory: ./tests
|
||||
- name: run snippets
|
||||
run: pipenv run pytest -v
|
||||
working-directory: ./tests
|
||||
env:
|
||||
RUSTPYTHON_DEBUG: 'true'
|
||||
- name: run cpython tests
|
||||
run: cargo run -- -m test -v
|
||||
env:
|
||||
RUSTPYTHONPATH: ${{ github.workspace }}/Lib
|
||||
- uses: actions-rs/grcov@v0.1
|
||||
id: coverage
|
||||
- name: upload to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
|
||||
testdata:
|
||||
name: Collect regression test data
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@master
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --verbose --all
|
||||
- name: collect tests data
|
||||
run: cargo run --release extra_tests/jsontests.py
|
||||
run: cargo run --release tests/jsontests.py
|
||||
env:
|
||||
RUSTPYTHONPATH: ${{ github.workspace }}/Lib
|
||||
- name: upload tests data to the website
|
||||
@@ -64,91 +82,7 @@ jobs:
|
||||
|
||||
git clone git@github.com:RustPython/rustpython.github.io.git website
|
||||
cd website
|
||||
cp ../extra_tests/cpython_tests_results.json ./_data/regrtests_results.json
|
||||
cp ../tests/cpython_tests_results.json ./_data/regrtests_results.json
|
||||
git add ./_data/regrtests_results.json
|
||||
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update regression test results" --author="$GITHUB_ACTOR"; then
|
||||
git push
|
||||
fi
|
||||
|
||||
whatsleft:
|
||||
name: Collect what is left data
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: build rustpython
|
||||
run: cargo build --release --verbose
|
||||
- name: Collect what is left data
|
||||
run: |
|
||||
chmod +x ./whats_left.py
|
||||
./whats_left.py --features "ssl,sqlite" > whats_left.temp
|
||||
env:
|
||||
RUSTPYTHONPATH: ${{ github.workspace }}/Lib
|
||||
- name: Upload data to the website
|
||||
env:
|
||||
SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
echo "$SSHKEY" >~/github_key
|
||||
chmod 600 ~/github_key
|
||||
export GIT_SSH_COMMAND="ssh -i ~/github_key"
|
||||
|
||||
git clone git@github.com:RustPython/rustpython.github.io.git website
|
||||
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
|
||||
fi
|
||||
|
||||
benchmark:
|
||||
name: Collect benchmark data
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
- run: cargo install cargo-criterion
|
||||
- name: build benchmarks
|
||||
run: cargo build --release --benches
|
||||
- name: collect execution benchmark data
|
||||
run: cargo criterion --bench execution
|
||||
- name: collect microbenchmarks data
|
||||
run: cargo criterion --bench microbenchmarks
|
||||
- name: restructure generated files
|
||||
run: |
|
||||
cd ./target/criterion/reports
|
||||
find -type d -name cpython | xargs rm -rf
|
||||
find -type d -name rustpython | xargs rm -rf
|
||||
find -mindepth 2 -maxdepth 2 -name violin.svg | xargs rm -rf
|
||||
find -type f -not -name violin.svg | xargs rm -rf
|
||||
for file in $(find -type f -name violin.svg); do mv $file $(echo $file | sed -E "s_\./([^/]+)/([^/]+)/violin\.svg_./\1/\2.svg_"); done
|
||||
find -mindepth 2 -maxdepth 2 -type d | xargs rm -rf
|
||||
cd ..
|
||||
mv reports/* .
|
||||
rmdir reports
|
||||
- name: upload benchmark data to the website
|
||||
env:
|
||||
SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }}
|
||||
run: |
|
||||
echo "$SSHKEY" >~/github_key
|
||||
chmod 600 ~/github_key
|
||||
export GIT_SSH_COMMAND="ssh -i ~/github_key"
|
||||
|
||||
git clone git@github.com:RustPython/rustpython.github.io.git website
|
||||
cd website
|
||||
rm -rf ./assets/criterion
|
||||
cp -r ../target/criterion ./assets/criterion
|
||||
git add ./assets/criterion
|
||||
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update benchmark results"; then
|
||||
git push
|
||||
fi
|
||||
git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update regression test results" --author="$GITHUB_ACTOR"
|
||||
git push
|
||||
|
||||
173
.github/workflows/release.yml
vendored
173
.github/workflows/release.yml
vendored
@@ -1,173 +0,0 @@
|
||||
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-*
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -2,22 +2,17 @@
|
||||
/*/target
|
||||
**/*.rs.bk
|
||||
**/*.bytecode
|
||||
__pycache__/
|
||||
__pycache__
|
||||
**/*.pytest_cache
|
||||
.*sw*
|
||||
.repl_history.txt
|
||||
.vscode/
|
||||
.vscode
|
||||
wasm-pack.log
|
||||
.idea/
|
||||
.envrc
|
||||
.python-version
|
||||
tests/snippets/resources
|
||||
|
||||
flame-graph.html
|
||||
flame.txt
|
||||
flamescope.json
|
||||
/wapm.lock
|
||||
/wapm_packages
|
||||
/.cargo/config
|
||||
|
||||
extra_tests/snippets/resources
|
||||
extra_tests/not_impl.py
|
||||
|
||||
7
.gitpod.Dockerfile
vendored
7
.gitpod.Dockerfile
vendored
@@ -11,11 +11,4 @@ RUN rm -rf ~/.rustup && \
|
||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh && \
|
||||
rustup target add wasm32-unknown-unknown
|
||||
|
||||
RUN sudo apt-get -q update \
|
||||
&& sudo apt-get install -yq \
|
||||
libpython3.6 \
|
||||
rust-lldb \
|
||||
&& sudo rm -rf /var/lib/apt/lists/*
|
||||
ENV RUST_LLDB=/usr/bin/lldb-8
|
||||
|
||||
USER root
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
image:
|
||||
file: .gitpod.Dockerfile
|
||||
|
||||
vscode:
|
||||
extensions:
|
||||
- vadimcn.vscode-lldb@1.5.3:vTh/rWhvJ5nQpeAVsD20QA==
|
||||
8
.mailmap
8
.mailmap
@@ -1,8 +0,0 @@
|
||||
#
|
||||
# This list is used by git-shortlog to aggregate contributions. It is
|
||||
# necessary when either the author's full name is not always written
|
||||
# the same way, and/or the same author contributes from different
|
||||
# email addresses.
|
||||
#
|
||||
|
||||
Noa <coolreader18@gmail.com> <33094578+coolreader18@users.noreply.github.com>
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug Rust Code",
|
||||
//"preLaunchTask": "cargo",
|
||||
"program": "${workspaceFolder}/target/debug/rustpython",
|
||||
"cwd": "${workspaceFolder}",
|
||||
//"valuesFormatting": "parseText"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"cpp.buildConfigurations": [
|
||||
{
|
||||
"name": "",
|
||||
"directory": ""
|
||||
},
|
||||
]
|
||||
}
|
||||
298
.vscode/launch.json
vendored
298
.vscode/launch.json
vendored
@@ -1,298 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'rustpython'",
|
||||
"preLaunchTask": "Build RustPython Debug",
|
||||
"program": "target/debug/rustpython",
|
||||
"args": [],
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'rustpython' without SSL",
|
||||
"preLaunchTask": "Build RustPython Debug without SSL",
|
||||
"program": "target/debug/rustpython",
|
||||
"args": [],
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug benchmark 'execution'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bench=execution",
|
||||
"--package=rustpython"
|
||||
],
|
||||
"filter": {
|
||||
"name": "execution",
|
||||
"kind": "bench"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug benchmark 'microbenchmarks'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bench=microbenchmarks",
|
||||
"--package=rustpython"
|
||||
],
|
||||
"filter": {
|
||||
"name": "microbenchmarks",
|
||||
"kind": "bench"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-pylib'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-pylib"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-pylib",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-bytecode'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-bytecode"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-bytecode",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-compiler'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-compiler"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-compiler",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-compiler-core'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-compiler-core"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-compiler-core",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-ast'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-ast"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-ast",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-parser'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-parser"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-parser",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-vm'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-vm"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-vm",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-common'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-common"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-common",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython-jit'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython-jit"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython-jit",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug integration test 'integration'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--test=integration",
|
||||
"--package=rustpython-jit"
|
||||
],
|
||||
"filter": {
|
||||
"name": "integration",
|
||||
"kind": "test"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in library 'rustpython_wasm'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--lib",
|
||||
"--package=rustpython_wasm"
|
||||
],
|
||||
"filter": {
|
||||
"name": "rustpython_wasm",
|
||||
"kind": "lib"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
36
.vscode/tasks.json
vendored
36
.vscode/tasks.json
vendored
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build RustPython Debug without SSL",
|
||||
"type": "shell",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$rustc",
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"label": "Build RustPython Debug",
|
||||
"type": "shell",
|
||||
"command": "cargo",
|
||||
"args": [
|
||||
"build",
|
||||
"--features=ssl"
|
||||
],
|
||||
"problemMatcher": [
|
||||
"$rustc",
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
3690
Cargo.lock
generated
3690
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
255
Cargo.toml
255
Cargo.toml
@@ -1,241 +1,54 @@
|
||||
[package]
|
||||
name = "rustpython"
|
||||
version = "0.1.2"
|
||||
authors = ["RustPython Team"]
|
||||
edition = "2018"
|
||||
description = "A python interpreter written in rust."
|
||||
include = ["LICENSE", "Cargo.toml", "src/**/*.rs"]
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
license.workspace = true
|
||||
repository = "https://github.com/RustPython/RustPython"
|
||||
license = "MIT"
|
||||
|
||||
[workspace]
|
||||
members = [".", "derive", "vm", "wasm/lib", "parser", "compiler", "bytecode", "examples/freeze"]
|
||||
|
||||
[[bench]]
|
||||
name = "bench"
|
||||
path = "./benchmarks/bench.rs"
|
||||
|
||||
[features]
|
||||
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 = ["stdlib", "rustpython-vm/freeze-stdlib", "rustpython-pylib?/freeze-stdlib"]
|
||||
jit = ["rustpython-vm/jit"]
|
||||
threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"]
|
||||
sqlite = ["rustpython-stdlib/sqlite"]
|
||||
ssl = ["rustpython-stdlib/ssl"]
|
||||
ssl-vendor = ["ssl", "rustpython-stdlib/ssl-vendor"]
|
||||
tkinter = ["rustpython-stdlib/tkinter"]
|
||||
freeze-stdlib = ["rustpython-vm/freeze-stdlib"]
|
||||
|
||||
[build-dependencies]
|
||||
winresource = "0.1"
|
||||
ssl = ["rustpython-vm/ssl"]
|
||||
|
||||
[dependencies]
|
||||
rustpython-compiler = { workspace = true }
|
||||
rustpython-pylib = { workspace = true, optional = true }
|
||||
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
|
||||
rustpython-vm = { workspace = true, features = ["compiler"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
log = "0.4"
|
||||
env_logger = "0.7"
|
||||
clap = "2.33"
|
||||
rustpython-compiler = {path = "compiler", version = "0.1.1"}
|
||||
rustpython-parser = {path = "parser", version = "0.1.1"}
|
||||
rustpython-vm = {path = "vm", version = "0.1.1"}
|
||||
dirs = { package = "dirs-next", version = "1.0" }
|
||||
num-traits = "0.2.8"
|
||||
cfg-if = "0.1"
|
||||
|
||||
cfg-if = { workspace = true }
|
||||
log = { workspace = true }
|
||||
flame = { workspace = true, optional = true }
|
||||
flame = { version = "0.2", optional = true }
|
||||
flamescope = { version = "0.1", optional = true }
|
||||
|
||||
lexopt = "0.3"
|
||||
dirs = { package = "dirs-next", version = "2.0" }
|
||||
env_logger = "0.11"
|
||||
flamescope = { version = "0.1.2", optional = true }
|
||||
[target.'cfg(not(target_os = "wasi"))'.dependencies]
|
||||
rustyline = "6.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
libc = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rustyline = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
pyo3 = { version = "0.24", features = ["auto-initialize"] }
|
||||
|
||||
[[bench]]
|
||||
name = "execution"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "microbenchmarks"
|
||||
harness = false
|
||||
[dev-dependencies.cpython]
|
||||
version = "0.2"
|
||||
|
||||
[[bin]]
|
||||
name = "rustpython"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[profile.test]
|
||||
opt-level = 3
|
||||
# https://github.com/rust-lang/rust/issues/92869
|
||||
# lto = "thin"
|
||||
|
||||
[profile.bench]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
|
||||
[patch.crates-io]
|
||||
# REDOX START, Uncomment when you want to compile/check with redoxer
|
||||
# REDOX START, Uncommment when you want to compile/check with redoxer
|
||||
# # following patches are just waiting on a new version to be released to crates.io
|
||||
# nix = { git = "https://github.com/nix-rust/nix" }
|
||||
# crossbeam-utils = { git = "https://github.com/crossbeam-rs/crossbeam" }
|
||||
# socket2 = { git = "https://github.com/alexcrichton/socket2-rs" }
|
||||
# REDOX END
|
||||
|
||||
# Used only on Windows to build the vcpkg dependencies
|
||||
[package.metadata.vcpkg]
|
||||
git = "https://github.com/microsoft/vcpkg"
|
||||
# The revision of the vcpkg repository to use
|
||||
# https://github.com/microsoft/vcpkg/tags
|
||||
rev = "2024.02.14"
|
||||
|
||||
[package.metadata.vcpkg.target]
|
||||
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md", dev-dependencies = ["openssl" ] }
|
||||
|
||||
[package.metadata.packager]
|
||||
product-name = "RustPython"
|
||||
identifier = "com.rustpython.rustpython"
|
||||
description = "An open source Python 3 interpreter written in Rust"
|
||||
homepage = "https://rustpython.github.io/"
|
||||
license_file = "LICENSE"
|
||||
authors = ["RustPython Team"]
|
||||
publisher = "RustPython Team"
|
||||
resources = ["LICENSE", "README.md", "Lib"]
|
||||
icons = ["32x32.png"]
|
||||
|
||||
[package.metadata.packager.nsis]
|
||||
installer_mode = "both"
|
||||
template = "installer-config/installer.nsi"
|
||||
|
||||
[package.metadata.packager.wix]
|
||||
template = "installer-config/installer.wxs"
|
||||
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"compiler",
|
||||
"compiler/core",
|
||||
"compiler/codegen",
|
||||
"compiler/literal",
|
||||
".",
|
||||
"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"
|
||||
|
||||
109
DEVELOPMENT.md
109
DEVELOPMENT.md
@@ -19,21 +19,17 @@ The contents of the Development Guide include:
|
||||
|
||||
RustPython requires the following:
|
||||
|
||||
- Rust latest stable version (e.g 1.69.0 as of Apr 20 2023)
|
||||
- Rust latest stable version (e.g 1.38.0 at Oct 1st 2019)
|
||||
- To check Rust version: `rustc --version`
|
||||
- If you have `rustup` on your system, enter to update to the latest
|
||||
stable version: `rustup update stable`
|
||||
- If you do not have Rust installed, use [rustup](https://rustup.rs/) to
|
||||
do so.
|
||||
- CPython version 3.13 or higher
|
||||
- CPython version 3.7.4 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
|
||||
[Anaconda](https://www.anaconda.com/distribution/).
|
||||
- [macOS] In case of libffi-sys compilation error, make sure autoconf, automake,
|
||||
libtool are installed
|
||||
- To install with [Homebrew](https://brew.sh), enter
|
||||
`brew install autoconf automake libtool`
|
||||
- [Optional] The Python package, `pytest`, is used for testing Python code
|
||||
snippets. To install, enter `python3 -m pip install pytest`.
|
||||
|
||||
@@ -41,58 +37,27 @@ RustPython requires the following:
|
||||
|
||||
The Rust code style used is the default
|
||||
[rustfmt](https://github.com/rust-lang/rustfmt) codestyle. Please format your
|
||||
code accordingly, or run `cargo fmt` to autoformat it. We also use
|
||||
[clippy](https://github.com/rust-lang/rust-clippy) to lint Rust code, which
|
||||
you can check yourself with `cargo clippy`.
|
||||
code accordingly. We also use [clippy](https://github.com/rust-lang/rust-clippy)
|
||||
to detect rust code issues.
|
||||
|
||||
Custom Python code (i.e. code not copied from CPython's standard library) should
|
||||
follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style. We also use
|
||||
[ruff](https://beta.ruff.rs/docs/) to check Python code style.
|
||||
|
||||
In addition to language specific tools, [cspell](https://github.com/streetsidesoftware/cspell),
|
||||
a code spell checker, is used in order to ensure correct spellings for code.
|
||||
Python code should follow the
|
||||
[PEP 8](https://www.python.org/dev/peps/pep-0008/) style. We also use
|
||||
[flake8](http://flake8.pycqa.org/en/latest/) to check Python code style.
|
||||
|
||||
## Testing
|
||||
|
||||
To test RustPython's functionality, a collection of Python snippets is located
|
||||
in the `extra_tests/snippets` directory and can be run using `pytest`:
|
||||
in the `tests/snippets` directory and can be run using `pytest`:
|
||||
|
||||
```shell
|
||||
$ cd extra_tests
|
||||
$ cd tests
|
||||
$ pytest -v
|
||||
```
|
||||
|
||||
Rust unit tests can be run with `cargo`:
|
||||
|
||||
```shell
|
||||
$ cargo test --workspace --exclude rustpython_wasm
|
||||
```
|
||||
|
||||
Python unit tests can be run by compiling RustPython and running the test module:
|
||||
|
||||
```shell
|
||||
$ cargo run --release -- -m test
|
||||
```
|
||||
|
||||
There are a few test options that are especially useful:
|
||||
|
||||
- `-j <n>` enables parallel testing (which is a lot faster), where `<n>` is the
|
||||
number of threads to be used, ideally the same as number of cores on your CPU.
|
||||
If you don't know, `-j 4` or `-j 8` are good options.
|
||||
- `-v` enables verbose mode, adding additional information about the tests being
|
||||
run.
|
||||
- `<test_name>` specifies a single test to run instead of running all tests.
|
||||
|
||||
For example, to run all tests in parallel:
|
||||
|
||||
```shell
|
||||
$ cargo run --release -- -m test -j 4
|
||||
```
|
||||
|
||||
To run only `test_cmath` (located at `Lib/test/test_cmath`) verbosely:
|
||||
|
||||
```shell
|
||||
$ cargo run --release -- -m test test_cmath -v
|
||||
$ cargo test --all
|
||||
```
|
||||
|
||||
## Profiling
|
||||
@@ -118,29 +83,34 @@ exists a raw html viewer which is currently broken, and we welcome a PR to fix i
|
||||
Understanding a new codebase takes time. Here's a brief view of the
|
||||
repository's structure:
|
||||
|
||||
- `bytecode/src`: python bytecode representation in rust structures
|
||||
- `compiler/src`: python compilation to bytecode
|
||||
- `core/src`: python bytecode representation in rust structures
|
||||
- `parser/src`: python lexing, parsing and ast
|
||||
- `derive/src`: Rust language extensions and macros specific to rustpython
|
||||
- `parser/src`: python lexing, parsing and ast
|
||||
- `Lib`: Carefully selected / copied files from CPython sourcecode. This is
|
||||
the python side of the standard library.
|
||||
- `test`: CPython test suite
|
||||
- `vm/src`: python virtual machine
|
||||
- `builtins`: Builtin functions and types
|
||||
- `builtins.rs`: Builtin functions
|
||||
- `compile.rs`: the python compiler from ast to bytecode
|
||||
- `obj`: python builtin types
|
||||
- `stdlib`: Standard library parts implemented in rust.
|
||||
- `src`: using the other subcrates to bring rustpython to life.
|
||||
- `docs`: documentation (work in progress)
|
||||
- `py_code_object`: CPython bytecode to rustpython bytecode converter (work in
|
||||
progress)
|
||||
- `wasm`: Binary crate and resources for WebAssembly build
|
||||
- `extra_tests`: extra integration test snippets as a supplement to `Lib/test`
|
||||
- `tests`: integration test snippets
|
||||
|
||||
## Understanding Internals
|
||||
|
||||
The RustPython workspace includes the `rustpython` top-level crate. The `Cargo.toml`
|
||||
file in the root of the repo provide configuration of the crate and the
|
||||
implementation is found in the `src` directory (specifically, `src/lib.rs`).
|
||||
implementation is found in the `src` directory (specifically,
|
||||
`src/main.rs`).
|
||||
|
||||
The top-level `rustpython` binary depends on several lower-level crates including:
|
||||
|
||||
- `rustpython-parser` (implementation in `compiler/parser/src`)
|
||||
- `rustpython-parser` (implementation in `parser/src`)
|
||||
- `rustpython-compiler` (implementation in `compiler/src`)
|
||||
- `rustpython-vm` (implementation in `vm/src`)
|
||||
|
||||
@@ -158,26 +128,25 @@ enable a line of code to go through a series of steps:
|
||||
This crate contains the lexer and parser to convert a line of code to
|
||||
an Abstract Syntax Tree (AST):
|
||||
|
||||
- Lexer: `compiler/parser/src/lexer.rs` converts Python source code into tokens
|
||||
- Parser: `compiler/parser/src/parser.rs` takes the tokens generated by the lexer and parses
|
||||
- Lexer: `parser/lexer.rs` converts Python source code into tokens
|
||||
- Parser: `parser/parser.rs` takes the tokens generated by the lexer and parses
|
||||
the tokens into an AST (Abstract Syntax Tree) where the nodes of the syntax
|
||||
tree are Rust structs and enums.
|
||||
- The Parser relies on `LALRPOP`, a Rust parser generator framework. The
|
||||
LALRPOP definition of Python's grammar is in `compiler/parser/src/python.lalrpop`.
|
||||
- The Parser relies on `LALRPOP`, a Rust parser generator framework.
|
||||
- More information on parsers and a tutorial can be found in the
|
||||
[LALRPOP book](https://lalrpop.github.io/lalrpop/).
|
||||
- AST: `compiler/ast/` implements in Rust the Python types and expressions
|
||||
[LALRPOP book](https://lalrpop.github.io/lalrpop/README.html).
|
||||
- AST: `parser/ast.rs` implements in Rust the Python types and expressions
|
||||
represented by the AST nodes.
|
||||
|
||||
### rustpython-compiler
|
||||
|
||||
The `rustpython-compiler` crate's purpose is to transform the AST (Abstract Syntax
|
||||
Tree) to bytecode. The implementation of the compiler is found in the
|
||||
`compiler/src` directory. The compiler implements Python's symbol table,
|
||||
ast->bytecode compiler, and bytecode optimizer in Rust.
|
||||
`compiler/src` directory. The compiler implements Python's peephole optimizer
|
||||
implementation, Symbol table, and streams in Rust.
|
||||
|
||||
Implementation of bytecode structure in Rust is found in the `compiler/core/src`
|
||||
directory. `compiler/core/src/bytecode.rs` contains the representation of
|
||||
Implementation of bytecode structure in Rust is found in the `bytecode/src`
|
||||
directory. The `bytecode/src/bytecode.rs` contains the representation of
|
||||
instructions and operations in Rust. Further information about Python's
|
||||
bytecode instructions can be found in the
|
||||
[Python documentation](https://docs.python.org/3/library/dis.html#bytecodes).
|
||||
@@ -189,20 +158,10 @@ executes Python's instructions. The `vm/src` directory contains code to
|
||||
implement the read and evaluation loop that fetches and dispatches
|
||||
instructions. This directory also contains the implementation of the
|
||||
Python Standard Library modules in Rust (`vm/src/stdlib`). In Python
|
||||
everything can be represented as an object. The `vm/src/builtins` directory holds
|
||||
the Rust code used to represent different Python objects and their methods. The
|
||||
core implementation of what a Python object is can be found in
|
||||
`vm/src/object/core.rs`.
|
||||
|
||||
### Code generation
|
||||
|
||||
There are some code generations involved in building RustPython:
|
||||
|
||||
- some part of the AST code is generated from `vm/src/stdlib/ast/gen.rs` to `compiler/ast/src/ast_gen.rs`.
|
||||
- the `__doc__` attributes are generated by the
|
||||
[__doc__](https://github.com/RustPython/__doc__) project which is then included as the `rustpython-doc` crate.
|
||||
everything can be represented as an Object. `vm/src/obj` directory holds
|
||||
the Rust code used to represent a Python Object and its methods.
|
||||
|
||||
## Questions
|
||||
|
||||
Have you tried these steps and have a question, please chat with us on
|
||||
[Discord](https://discord.gg/vru8NypEhv).
|
||||
[gitter](https://gitter.im/rustpython/Lobby).
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 RustPython Team
|
||||
Copyright (c) 2020 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
|
||||
|
||||
49
Lib/__future__.py
vendored
49
Lib/__future__.py
vendored
@@ -33,7 +33,7 @@ in releases at or after that, modules no longer need
|
||||
to use the feature in question, but may continue to use such imports.
|
||||
|
||||
MandatoryRelease may also be None, meaning that a planned feature got
|
||||
dropped or that the release version is undetermined.
|
||||
dropped.
|
||||
|
||||
Instances of class _Feature have two corresponding methods,
|
||||
.getOptionalRelease() and .getMandatoryRelease().
|
||||
@@ -42,7 +42,7 @@ CompilerFlag is the (bitfield) flag that should be passed in the fourth
|
||||
argument to the builtin function compile() to enable the feature in
|
||||
dynamically compiled code. This flag is stored in the .compiler_flag
|
||||
attribute on _Future instances. These values must match the appropriate
|
||||
#defines of CO_xxx flags in Include/cpython/compile.h.
|
||||
#defines of CO_xxx flags in Include/compile.h.
|
||||
|
||||
No feature line is ever to be deleted from this file.
|
||||
"""
|
||||
@@ -57,29 +57,25 @@ all_feature_names = [
|
||||
"unicode_literals",
|
||||
"barry_as_FLUFL",
|
||||
"generator_stop",
|
||||
"annotations",
|
||||
]
|
||||
|
||||
__all__ = ["all_feature_names"] + all_feature_names
|
||||
|
||||
# The CO_xxx symbols are defined here under the same names defined in
|
||||
# code.h and used by compile.h, so that an editor search will find them here.
|
||||
# However, they're not exported in __all__, because they don't really belong to
|
||||
# The CO_xxx symbols are defined here under the same names used by
|
||||
# compile.h, so that an editor search will find them here. However,
|
||||
# they're not exported in __all__, because they don't really belong to
|
||||
# this module.
|
||||
CO_NESTED = 0x0010 # nested_scopes
|
||||
CO_GENERATOR_ALLOWED = 0 # generators (obsolete, was 0x1000)
|
||||
CO_FUTURE_DIVISION = 0x20000 # division
|
||||
CO_FUTURE_ABSOLUTE_IMPORT = 0x40000 # perform absolute imports by default
|
||||
CO_FUTURE_WITH_STATEMENT = 0x80000 # with statement
|
||||
CO_FUTURE_PRINT_FUNCTION = 0x100000 # print function
|
||||
CO_FUTURE_UNICODE_LITERALS = 0x200000 # unicode string literals
|
||||
CO_FUTURE_BARRY_AS_BDFL = 0x400000
|
||||
CO_FUTURE_GENERATOR_STOP = 0x800000 # StopIteration becomes RuntimeError in generators
|
||||
CO_FUTURE_ANNOTATIONS = 0x1000000 # annotations become strings at runtime
|
||||
|
||||
CO_NESTED = 0x0010 # nested_scopes
|
||||
CO_GENERATOR_ALLOWED = 0 # generators (obsolete, was 0x1000)
|
||||
CO_FUTURE_DIVISION = 0x2000 # division
|
||||
CO_FUTURE_ABSOLUTE_IMPORT = 0x4000 # perform absolute imports by default
|
||||
CO_FUTURE_WITH_STATEMENT = 0x8000 # with statement
|
||||
CO_FUTURE_PRINT_FUNCTION = 0x10000 # print function
|
||||
CO_FUTURE_UNICODE_LITERALS = 0x20000 # unicode string literals
|
||||
CO_FUTURE_BARRY_AS_BDFL = 0x40000
|
||||
CO_FUTURE_GENERATOR_STOP = 0x80000 # StopIteration becomes RuntimeError in generators
|
||||
|
||||
class _Feature:
|
||||
|
||||
def __init__(self, optionalRelease, mandatoryRelease, compiler_flag):
|
||||
self.optional = optionalRelease
|
||||
self.mandatory = mandatoryRelease
|
||||
@@ -90,14 +86,16 @@ class _Feature:
|
||||
|
||||
This is a 5-tuple, of the same form as sys.version_info.
|
||||
"""
|
||||
|
||||
return self.optional
|
||||
|
||||
def getMandatoryRelease(self):
|
||||
"""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, or the release date is undetermined, is None.
|
||||
the feature was dropped, is None.
|
||||
"""
|
||||
|
||||
return self.mandatory
|
||||
|
||||
def __repr__(self):
|
||||
@@ -105,7 +103,6 @@ class _Feature:
|
||||
self.mandatory,
|
||||
self.compiler_flag))
|
||||
|
||||
|
||||
nested_scopes = _Feature((2, 1, 0, "beta", 1),
|
||||
(2, 2, 0, "alpha", 0),
|
||||
CO_NESTED)
|
||||
@@ -135,13 +132,9 @@ unicode_literals = _Feature((2, 6, 0, "alpha", 2),
|
||||
CO_FUTURE_UNICODE_LITERALS)
|
||||
|
||||
barry_as_FLUFL = _Feature((3, 1, 0, "alpha", 2),
|
||||
(4, 0, 0, "alpha", 0),
|
||||
CO_FUTURE_BARRY_AS_BDFL)
|
||||
(3, 9, 0, "alpha", 0),
|
||||
CO_FUTURE_BARRY_AS_BDFL)
|
||||
|
||||
generator_stop = _Feature((3, 5, 0, "beta", 1),
|
||||
(3, 7, 0, "alpha", 0),
|
||||
CO_FUTURE_GENERATOR_STOP)
|
||||
|
||||
annotations = _Feature((3, 7, 0, "beta", 1),
|
||||
None,
|
||||
CO_FUTURE_ANNOTATIONS)
|
||||
(3, 7, 0, "alpha", 0),
|
||||
CO_FUTURE_GENERATOR_STOP)
|
||||
|
||||
16
Lib/__hello__.py
vendored
16
Lib/__hello__.py
vendored
@@ -1,16 +0,0 @@
|
||||
initialized = True
|
||||
|
||||
class TestFrozenUtf8_1:
|
||||
"""\u00b6"""
|
||||
|
||||
class TestFrozenUtf8_2:
|
||||
"""\u03c0"""
|
||||
|
||||
class TestFrozenUtf8_4:
|
||||
"""\U0001f600"""
|
||||
|
||||
def main():
|
||||
print("Hello world!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,7 +0,0 @@
|
||||
initialized = True
|
||||
|
||||
def main():
|
||||
print("Hello world!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,7 +0,0 @@
|
||||
initialized = True
|
||||
|
||||
def main():
|
||||
print("Hello world!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
108
Lib/_aix_support.py
vendored
108
Lib/_aix_support.py
vendored
@@ -1,108 +0,0 @@
|
||||
"""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
181
Lib/_android_support.py
vendored
@@ -1,181 +0,0 @@
|
||||
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
66
Lib/_apple_support.py
vendored
@@ -1,66 +0,0 @@
|
||||
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)
|
||||
817
Lib/_pycodecs.py → Lib/_codecs.py
vendored
817
Lib/_pycodecs.py → Lib/_codecs.py
vendored
File diff suppressed because it is too large
Load Diff
275
Lib/_collections_abc.py
vendored
275
Lib/_collections_abc.py
vendored
@@ -6,41 +6,9 @@
|
||||
Unit tests are in test_collections.
|
||||
"""
|
||||
|
||||
############ Maintenance notes #########################################
|
||||
#
|
||||
# ABCs are different from other standard library modules in that they
|
||||
# specify compliance tests. In general, once an ABC has been published,
|
||||
# new methods (either abstract or concrete) cannot be added.
|
||||
#
|
||||
# Though classes that inherit from an ABC would automatically receive a
|
||||
# new mixin method, registered classes would become non-compliant and
|
||||
# violate the contract promised by ``isinstance(someobj, SomeABC)``.
|
||||
#
|
||||
# Though irritating, the correct procedure for adding new abstract or
|
||||
# mixin methods is to create a new ABC as a subclass of the previous
|
||||
# ABC. For example, union(), intersection(), and difference() cannot
|
||||
# be added to Set but could go into a new ABC that extends Set.
|
||||
#
|
||||
# Because they are so hard to change, new ABCs should have their APIs
|
||||
# carefully thought through prior to publication.
|
||||
#
|
||||
# Since ABCMeta only checks for the presence of methods, it is possible
|
||||
# to alter the signature of a method by adding optional arguments
|
||||
# or changing parameters names. This is still a bit dubious but at
|
||||
# least it won't cause isinstance() to return an incorrect result.
|
||||
#
|
||||
#
|
||||
#######################################################################
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
import sys
|
||||
|
||||
GenericAlias = type(list[int])
|
||||
EllipsisType = type(...)
|
||||
def _f(): pass
|
||||
FunctionType = type(_f)
|
||||
del _f
|
||||
|
||||
__all__ = ["Awaitable", "Coroutine",
|
||||
"AsyncIterable", "AsyncIterator", "AsyncGenerator",
|
||||
"Hashable", "Iterable", "Iterator", "Generator", "Reversible",
|
||||
@@ -49,7 +17,7 @@ __all__ = ["Awaitable", "Coroutine",
|
||||
"Mapping", "MutableMapping",
|
||||
"MappingView", "KeysView", "ItemsView", "ValuesView",
|
||||
"Sequence", "MutableSequence",
|
||||
"ByteString", "Buffer",
|
||||
"ByteString",
|
||||
]
|
||||
|
||||
# This module has been renamed from collections.abc to _collections_abc to
|
||||
@@ -85,10 +53,6 @@ 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
|
||||
@@ -96,14 +60,15 @@ _coro = _coro()
|
||||
coroutine = type(_coro)
|
||||
_coro.close() # Prevent ResourceWarning
|
||||
del _coro
|
||||
## asynchronous generator ##
|
||||
async def _ag(): yield
|
||||
_ag = _ag()
|
||||
async_generator = type(_ag)
|
||||
del _ag
|
||||
# XXX RustPython TODO: async generators
|
||||
# ## asynchronous generator ##
|
||||
# async def _ag(): yield
|
||||
# _ag = _ag()
|
||||
# async_generator = type(_ag)
|
||||
# del _ag
|
||||
|
||||
|
||||
### ONE-TRICK PONIES ###
|
||||
# ## ONE-TRICK PONIES ###
|
||||
|
||||
def _check_methods(C, *methods):
|
||||
mro = C.__mro__
|
||||
@@ -146,8 +111,6 @@ class Awaitable(metaclass=ABCMeta):
|
||||
return _check_methods(C, "__await__")
|
||||
return NotImplemented
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
|
||||
class Coroutine(Awaitable):
|
||||
|
||||
@@ -207,8 +170,6 @@ class AsyncIterable(metaclass=ABCMeta):
|
||||
return _check_methods(C, "__aiter__")
|
||||
return NotImplemented
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
|
||||
class AsyncIterator(AsyncIterable):
|
||||
|
||||
@@ -277,7 +238,7 @@ class AsyncGenerator(AsyncIterator):
|
||||
return NotImplemented
|
||||
|
||||
|
||||
AsyncGenerator.register(async_generator)
|
||||
# AsyncGenerator.register(async_generator)
|
||||
|
||||
|
||||
class Iterable(metaclass=ABCMeta):
|
||||
@@ -295,8 +256,6 @@ class Iterable(metaclass=ABCMeta):
|
||||
return _check_methods(C, "__iter__")
|
||||
return NotImplemented
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
|
||||
class Iterator(Iterable):
|
||||
|
||||
@@ -316,10 +275,9 @@ class Iterator(Iterable):
|
||||
return _check_methods(C, '__iter__', '__next__')
|
||||
return NotImplemented
|
||||
|
||||
|
||||
Iterator.register(bytes_iterator)
|
||||
Iterator.register(bytearray_iterator)
|
||||
#Iterator.register(callable_iterator)
|
||||
# Iterator.register(callable_iterator)
|
||||
Iterator.register(dict_keyiterator)
|
||||
Iterator.register(dict_valueiterator)
|
||||
Iterator.register(dict_itemiterator)
|
||||
@@ -396,10 +354,8 @@ class Generator(Iterator):
|
||||
'send', 'throw', 'close')
|
||||
return NotImplemented
|
||||
|
||||
|
||||
Generator.register(generator)
|
||||
|
||||
|
||||
class Sized(metaclass=ABCMeta):
|
||||
|
||||
__slots__ = ()
|
||||
@@ -429,9 +385,6 @@ class Container(metaclass=ABCMeta):
|
||||
return _check_methods(C, "__contains__")
|
||||
return NotImplemented
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
|
||||
class Collection(Sized, Iterable, Container):
|
||||
|
||||
__slots__ = ()
|
||||
@@ -442,110 +395,6 @@ class Collection(Sized, Iterable, Container):
|
||||
return _check_methods(C, "__len__", "__iter__", "__contains__")
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class Buffer(metaclass=ABCMeta):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
def __buffer__(self, flags: int, /) -> memoryview:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, C):
|
||||
if cls is Buffer:
|
||||
return _check_methods(C, "__buffer__")
|
||||
return NotImplemented
|
||||
|
||||
|
||||
class _CallableGenericAlias(GenericAlias):
|
||||
""" Represent `Callable[argtypes, resulttype]`.
|
||||
|
||||
This sets ``__args__`` to a tuple containing the flattened ``argtypes``
|
||||
followed by ``resulttype``.
|
||||
|
||||
Example: ``Callable[[int, str], float]`` sets ``__args__`` to
|
||||
``(int, str, float)``.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __new__(cls, origin, args):
|
||||
if not (isinstance(args, tuple) and len(args) == 2):
|
||||
raise TypeError(
|
||||
"Callable must be used as Callable[[arg, ...], result].")
|
||||
t_args, t_result = args
|
||||
if isinstance(t_args, (tuple, list)):
|
||||
args = (*t_args, t_result)
|
||||
elif not _is_param_expr(t_args):
|
||||
raise TypeError(f"Expected a list of types, an ellipsis, "
|
||||
f"ParamSpec, or Concatenate. Got {t_args}")
|
||||
return super().__new__(cls, origin, args)
|
||||
|
||||
def __repr__(self):
|
||||
if len(self.__args__) == 2 and _is_param_expr(self.__args__[0]):
|
||||
return super().__repr__()
|
||||
return (f'collections.abc.Callable'
|
||||
f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], '
|
||||
f'{_type_repr(self.__args__[-1])}]')
|
||||
|
||||
def __reduce__(self):
|
||||
args = self.__args__
|
||||
if not (len(args) == 2 and _is_param_expr(args[0])):
|
||||
args = list(args[:-1]), args[-1]
|
||||
return _CallableGenericAlias, (Callable, args)
|
||||
|
||||
def __getitem__(self, item):
|
||||
# Called during TypeVar substitution, returns the custom subclass
|
||||
# rather than the default types.GenericAlias object. Most of the
|
||||
# code is copied from typing's _GenericAlias and the builtin
|
||||
# types.GenericAlias.
|
||||
if not isinstance(item, tuple):
|
||||
item = (item,)
|
||||
|
||||
new_args = super().__getitem__(item).__args__
|
||||
|
||||
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
|
||||
if not isinstance(new_args[0], (tuple, list)):
|
||||
t_result = new_args[-1]
|
||||
t_args = new_args[:-1]
|
||||
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
|
||||
"""
|
||||
if obj is Ellipsis:
|
||||
return True
|
||||
if isinstance(obj, list):
|
||||
return True
|
||||
obj = type(obj)
|
||||
names = ('ParamSpec', '_ConcatenateGenericAlias')
|
||||
return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names)
|
||||
|
||||
def _type_repr(obj):
|
||||
"""Return the repr() of an object, special-casing types (internal helper).
|
||||
|
||||
Copied from :mod:`typing` since collections.abc
|
||||
shouldn't depend on that module.
|
||||
(Keep this roughly in sync with the typing version.)
|
||||
"""
|
||||
if isinstance(obj, type):
|
||||
if obj.__module__ == 'builtins':
|
||||
return obj.__qualname__
|
||||
return f'{obj.__module__}.{obj.__qualname__}'
|
||||
if obj is Ellipsis:
|
||||
return '...'
|
||||
if isinstance(obj, FunctionType):
|
||||
return obj.__name__
|
||||
return repr(obj)
|
||||
|
||||
|
||||
class Callable(metaclass=ABCMeta):
|
||||
|
||||
__slots__ = ()
|
||||
@@ -560,13 +409,12 @@ class Callable(metaclass=ABCMeta):
|
||||
return _check_methods(C, "__call__")
|
||||
return NotImplemented
|
||||
|
||||
__class_getitem__ = classmethod(_CallableGenericAlias)
|
||||
|
||||
|
||||
### SETS ###
|
||||
|
||||
|
||||
class Set(Collection):
|
||||
|
||||
"""A set is a finite, iterable container.
|
||||
|
||||
This class provides concrete generic implementations of all
|
||||
@@ -694,7 +542,6 @@ class Set(Collection):
|
||||
hx = hash(x)
|
||||
h ^= (hx ^ (hx << 16) ^ 89869747) * 3644798167
|
||||
h &= MASK
|
||||
h ^= (h >> 11) ^ (h >> 25)
|
||||
h = h * 69069 + 907133923
|
||||
h &= MASK
|
||||
if h > MAX:
|
||||
@@ -703,7 +550,6 @@ class Set(Collection):
|
||||
h = 590923713
|
||||
return h
|
||||
|
||||
|
||||
Set.register(frozenset)
|
||||
|
||||
|
||||
@@ -786,25 +632,24 @@ class MutableSet(Set):
|
||||
self.discard(value)
|
||||
return self
|
||||
|
||||
|
||||
MutableSet.register(set)
|
||||
|
||||
|
||||
### MAPPINGS ###
|
||||
|
||||
|
||||
class Mapping(Collection):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
"""A Mapping is a generic container for associating key/value
|
||||
pairs.
|
||||
|
||||
This class provides concrete generic implementations of all
|
||||
methods except for __getitem__, __iter__, and __len__.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
# Tell ABCMeta.__new__ that this class should have TPFLAGS_MAPPING set.
|
||||
__abc_tpflags__ = 1 << 6 # Py_TPFLAGS_MAPPING
|
||||
|
||||
@abstractmethod
|
||||
def __getitem__(self, key):
|
||||
raise KeyError
|
||||
@@ -844,7 +689,6 @@ class Mapping(Collection):
|
||||
__reversed__ = None
|
||||
|
||||
Mapping.register(mappingproxy)
|
||||
Mapping.register(framelocalsproxy)
|
||||
|
||||
|
||||
class MappingView(Sized):
|
||||
@@ -860,15 +704,13 @@ class MappingView(Sized):
|
||||
def __repr__(self):
|
||||
return '{0.__class__.__name__}({0._mapping!r})'.format(self)
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
|
||||
class KeysView(MappingView, Set):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def _from_iterable(cls, it):
|
||||
def _from_iterable(self, it):
|
||||
return set(it)
|
||||
|
||||
def __contains__(self, key):
|
||||
@@ -877,7 +719,6 @@ class KeysView(MappingView, Set):
|
||||
def __iter__(self):
|
||||
yield from self._mapping
|
||||
|
||||
|
||||
KeysView.register(dict_keys)
|
||||
|
||||
|
||||
@@ -886,7 +727,7 @@ class ItemsView(MappingView, Set):
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def _from_iterable(cls, it):
|
||||
def _from_iterable(self, it):
|
||||
return set(it)
|
||||
|
||||
def __contains__(self, item):
|
||||
@@ -902,7 +743,6 @@ class ItemsView(MappingView, Set):
|
||||
for key in self._mapping:
|
||||
yield (key, self._mapping[key])
|
||||
|
||||
|
||||
ItemsView.register(dict_items)
|
||||
|
||||
|
||||
@@ -921,20 +761,21 @@ class ValuesView(MappingView, Collection):
|
||||
for key in self._mapping:
|
||||
yield self._mapping[key]
|
||||
|
||||
|
||||
ValuesView.register(dict_values)
|
||||
|
||||
|
||||
class MutableMapping(Mapping):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
"""A MutableMapping is a generic container for associating
|
||||
key/value pairs.
|
||||
|
||||
This class provides concrete generic implementations of all
|
||||
methods except for __getitem__, __setitem__, __delitem__,
|
||||
__iter__, and __len__.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __setitem__(self, key, value):
|
||||
@@ -980,21 +821,34 @@ class MutableMapping(Mapping):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def update(self, other=(), /, **kwds):
|
||||
def update(*args, **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.keys(): D[k] = E[k]
|
||||
If E present and has a .keys() method, does: for k in E: 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
|
||||
'''
|
||||
if isinstance(other, Mapping):
|
||||
for key in other:
|
||||
self[key] = other[key]
|
||||
elif hasattr(other, "keys"):
|
||||
for key in other.keys():
|
||||
self[key] = other[key]
|
||||
else:
|
||||
for key, value in other:
|
||||
self[key] = value
|
||||
if not args:
|
||||
raise TypeError("descriptor 'update' of 'MutableMapping' object "
|
||||
"needs an argument")
|
||||
self, *args = args
|
||||
if len(args) > 1:
|
||||
raise TypeError('update expected at most 1 arguments, got %d' %
|
||||
len(args))
|
||||
if args:
|
||||
other = args[0]
|
||||
try:
|
||||
mapping_inst = isinstance(other, Mapping)
|
||||
except TypeError:
|
||||
mapping_inst = False
|
||||
if mapping_inst:
|
||||
for key in other:
|
||||
self[key] = other[key]
|
||||
elif hasattr(other, "keys"):
|
||||
for key in other.keys():
|
||||
self[key] = other[key]
|
||||
else:
|
||||
for key, value in other:
|
||||
self[key] = value
|
||||
for key, value in kwds.items():
|
||||
self[key] = value
|
||||
|
||||
@@ -1006,13 +860,14 @@ class MutableMapping(Mapping):
|
||||
self[key] = default
|
||||
return default
|
||||
|
||||
|
||||
MutableMapping.register(dict)
|
||||
|
||||
|
||||
### SEQUENCES ###
|
||||
|
||||
|
||||
class Sequence(Reversible, Collection):
|
||||
|
||||
"""All the operations on a read-only sequence.
|
||||
|
||||
Concrete subclasses must override __new__ or __init__,
|
||||
@@ -1021,9 +876,6 @@ class Sequence(Reversible, Collection):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
# Tell ABCMeta.__new__ that this class should have TPFLAGS_SEQUENCE set.
|
||||
__abc_tpflags__ = 1 << 5 # Py_TPFLAGS_SEQUENCE
|
||||
|
||||
@abstractmethod
|
||||
def __getitem__(self, index):
|
||||
raise IndexError
|
||||
@@ -1064,10 +916,10 @@ class Sequence(Reversible, Collection):
|
||||
while stop is None or i < stop:
|
||||
try:
|
||||
v = self[i]
|
||||
if v is value or v == value:
|
||||
return i
|
||||
except IndexError:
|
||||
break
|
||||
if v is value or v == value:
|
||||
return i
|
||||
i += 1
|
||||
raise ValueError
|
||||
|
||||
@@ -1080,27 +932,9 @@ Sequence.register(str)
|
||||
Sequence.register(range)
|
||||
Sequence.register(memoryview)
|
||||
|
||||
class _DeprecateByteStringMeta(ABCMeta):
|
||||
def __new__(cls, name, bases, namespace, **kwargs):
|
||||
if name != "ByteString":
|
||||
import warnings
|
||||
|
||||
warnings._deprecated(
|
||||
"collections.abc.ByteString",
|
||||
remove=(3, 14),
|
||||
)
|
||||
return super().__new__(cls, name, bases, namespace, **kwargs)
|
||||
class ByteString(Sequence):
|
||||
|
||||
def __instancecheck__(cls, instance):
|
||||
import warnings
|
||||
|
||||
warnings._deprecated(
|
||||
"collections.abc.ByteString",
|
||||
remove=(3, 14),
|
||||
)
|
||||
return super().__instancecheck__(instance)
|
||||
|
||||
class ByteString(Sequence, metaclass=_DeprecateByteStringMeta):
|
||||
"""This unifies bytes and bytearray.
|
||||
|
||||
XXX Should add all their methods.
|
||||
@@ -1113,13 +947,15 @@ ByteString.register(bytearray)
|
||||
|
||||
|
||||
class MutableSequence(Sequence):
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
"""All the operations on a read-write sequence.
|
||||
|
||||
Concrete subclasses must provide __new__ or __init__,
|
||||
__getitem__, __setitem__, __delitem__, __len__, and insert().
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __setitem__(self, index, value):
|
||||
@@ -1177,6 +1013,5 @@ class MutableSequence(Sequence):
|
||||
self.extend(values)
|
||||
return self
|
||||
|
||||
|
||||
MutableSequence.register(list)
|
||||
MutableSequence.register(bytearray) # Multiply inheriting, see ByteString
|
||||
|
||||
112
Lib/_colorize.py
vendored
112
Lib/_colorize.py
vendored
@@ -1,112 +0,0 @@
|
||||
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()
|
||||
8
Lib/_compat_pickle.py
vendored
8
Lib/_compat_pickle.py
vendored
@@ -148,14 +148,6 @@ except NameError:
|
||||
else:
|
||||
PYTHON2_EXCEPTIONS += ("WindowsError",)
|
||||
|
||||
# NOTE: RUSTPYTHON exceptions
|
||||
try:
|
||||
JitError
|
||||
except NameError:
|
||||
pass
|
||||
else:
|
||||
PYTHON2_EXCEPTIONS += ("JitError",)
|
||||
|
||||
for excname in PYTHON2_EXCEPTIONS:
|
||||
NAME_MAPPING[("exceptions", excname)] = ("builtins", excname)
|
||||
|
||||
|
||||
162
Lib/_compression.py
vendored
162
Lib/_compression.py
vendored
@@ -1,162 +0,0 @@
|
||||
"""Internal classes used by the gzip, lzma and bz2 modules"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
|
||||
BUFFER_SIZE = io.DEFAULT_BUFFER_SIZE # Compressed data read chunk size
|
||||
|
||||
|
||||
class BaseStream(io.BufferedIOBase):
|
||||
"""Mode-checking helper functions."""
|
||||
|
||||
def _check_not_closed(self):
|
||||
if self.closed:
|
||||
raise ValueError("I/O operation on closed file")
|
||||
|
||||
def _check_can_read(self):
|
||||
if not self.readable():
|
||||
raise io.UnsupportedOperation("File not open for reading")
|
||||
|
||||
def _check_can_write(self):
|
||||
if not self.writable():
|
||||
raise io.UnsupportedOperation("File not open for writing")
|
||||
|
||||
def _check_can_seek(self):
|
||||
if not self.readable():
|
||||
raise io.UnsupportedOperation("Seeking is only supported "
|
||||
"on files open for reading")
|
||||
if not self.seekable():
|
||||
raise io.UnsupportedOperation("The underlying file object "
|
||||
"does not support seeking")
|
||||
|
||||
|
||||
class DecompressReader(io.RawIOBase):
|
||||
"""Adapts the decompressor API to a RawIOBase reader API"""
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def __init__(self, fp, decomp_factory, trailing_error=(), **decomp_args):
|
||||
self._fp = fp
|
||||
self._eof = False
|
||||
self._pos = 0 # Current offset in decompressed stream
|
||||
|
||||
# Set to size of decompressed stream once it is known, for SEEK_END
|
||||
self._size = -1
|
||||
|
||||
# Save the decompressor factory and arguments.
|
||||
# If the file contains multiple compressed streams, each
|
||||
# stream will need a separate decompressor object. A new decompressor
|
||||
# object is also needed when implementing a backwards seek().
|
||||
self._decomp_factory = decomp_factory
|
||||
self._decomp_args = decomp_args
|
||||
self._decompressor = self._decomp_factory(**self._decomp_args)
|
||||
|
||||
# Exception class to catch from decompressor signifying invalid
|
||||
# trailing data to ignore
|
||||
self._trailing_error = trailing_error
|
||||
|
||||
def close(self):
|
||||
self._decompressor = None
|
||||
return super().close()
|
||||
|
||||
def seekable(self):
|
||||
return self._fp.seekable()
|
||||
|
||||
def readinto(self, b):
|
||||
with memoryview(b) as view, view.cast("B") as byte_view:
|
||||
data = self.read(len(byte_view))
|
||||
byte_view[:len(data)] = data
|
||||
return len(data)
|
||||
|
||||
def read(self, size=-1):
|
||||
if size < 0:
|
||||
return self.readall()
|
||||
|
||||
if not size or self._eof:
|
||||
return b""
|
||||
data = None # Default if EOF is encountered
|
||||
# Depending on the input data, our call to the decompressor may not
|
||||
# return any data. In this case, try again after reading another block.
|
||||
while True:
|
||||
if self._decompressor.eof:
|
||||
rawblock = (self._decompressor.unused_data or
|
||||
self._fp.read(BUFFER_SIZE))
|
||||
if not rawblock:
|
||||
break
|
||||
# Continue to next stream.
|
||||
self._decompressor = self._decomp_factory(
|
||||
**self._decomp_args)
|
||||
try:
|
||||
data = self._decompressor.decompress(rawblock, size)
|
||||
except self._trailing_error:
|
||||
# Trailing data isn't a valid compressed stream; ignore it.
|
||||
break
|
||||
else:
|
||||
if self._decompressor.needs_input:
|
||||
rawblock = self._fp.read(BUFFER_SIZE)
|
||||
if not rawblock:
|
||||
raise EOFError("Compressed file ended before the "
|
||||
"end-of-stream marker was reached")
|
||||
else:
|
||||
rawblock = b""
|
||||
data = self._decompressor.decompress(rawblock, size)
|
||||
if data:
|
||||
break
|
||||
if not data:
|
||||
self._eof = True
|
||||
self._size = self._pos
|
||||
return b""
|
||||
self._pos += len(data)
|
||||
return data
|
||||
|
||||
def readall(self):
|
||||
chunks = []
|
||||
# sys.maxsize means the max length of output buffer is unlimited,
|
||||
# so that the whole input buffer can be decompressed within one
|
||||
# .decompress() call.
|
||||
while data := self.read(sys.maxsize):
|
||||
chunks.append(data)
|
||||
|
||||
return b"".join(chunks)
|
||||
|
||||
# Rewind the file to the beginning of the data stream.
|
||||
def _rewind(self):
|
||||
self._fp.seek(0)
|
||||
self._eof = False
|
||||
self._pos = 0
|
||||
self._decompressor = self._decomp_factory(**self._decomp_args)
|
||||
|
||||
def seek(self, offset, whence=io.SEEK_SET):
|
||||
# Recalculate offset as an absolute file position.
|
||||
if whence == io.SEEK_SET:
|
||||
pass
|
||||
elif whence == io.SEEK_CUR:
|
||||
offset = self._pos + offset
|
||||
elif whence == io.SEEK_END:
|
||||
# Seeking relative to EOF - we need to know the file's size.
|
||||
if self._size < 0:
|
||||
while self.read(io.DEFAULT_BUFFER_SIZE):
|
||||
pass
|
||||
offset = self._size + offset
|
||||
else:
|
||||
raise ValueError("Invalid value for whence: {}".format(whence))
|
||||
|
||||
# Make it so that offset is the number of bytes to skip forward.
|
||||
if offset < self._pos:
|
||||
self._rewind()
|
||||
else:
|
||||
offset -= self._pos
|
||||
|
||||
# Read and discard data until we reach the desired position.
|
||||
while offset > 0:
|
||||
data = self.read(min(io.DEFAULT_BUFFER_SIZE, offset))
|
||||
if not data:
|
||||
break
|
||||
offset -= len(data)
|
||||
|
||||
return self._pos
|
||||
|
||||
def tell(self):
|
||||
"""Return the current file position."""
|
||||
return self._pos
|
||||
74
Lib/_dummy_os.py
vendored
74
Lib/_dummy_os.py
vendored
@@ -1,74 +0,0 @@
|
||||
"""
|
||||
A shim of the os module containing only simple path-related utilities
|
||||
"""
|
||||
|
||||
try:
|
||||
from os import *
|
||||
except ImportError:
|
||||
import abc, sys
|
||||
|
||||
def __getattr__(name):
|
||||
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
|
||||
|
||||
sys.modules['os'] = sys.modules['posix'] = sys.modules[__name__]
|
||||
|
||||
import posixpath as path
|
||||
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):
|
||||
"""Return the path representation of a path-like object.
|
||||
|
||||
If str or bytes is passed in, it is returned unchanged. Otherwise the
|
||||
os.PathLike interface is used to get the path representation. If the
|
||||
path representation is not str or bytes, TypeError is raised. If the
|
||||
provided path is not str, bytes, or os.PathLike, TypeError is raised.
|
||||
"""
|
||||
if isinstance(path, (str, bytes)):
|
||||
return path
|
||||
|
||||
# Work from the object's type to match method resolution of other magic
|
||||
# methods.
|
||||
path_type = type(path)
|
||||
try:
|
||||
path_repr = path_type.__fspath__(path)
|
||||
except AttributeError:
|
||||
if hasattr(path_type, '__fspath__'):
|
||||
raise
|
||||
else:
|
||||
raise TypeError("expected str, bytes or os.PathLike object, "
|
||||
"not " + path_type.__name__)
|
||||
if isinstance(path_repr, (str, bytes)):
|
||||
return path_repr
|
||||
else:
|
||||
raise TypeError("expected {}.__fspath__() to return str or bytes, "
|
||||
"not {}".format(path_type.__name__,
|
||||
type(path_repr).__name__))
|
||||
|
||||
class PathLike(abc.ABC):
|
||||
|
||||
"""Abstract base class for implementing the file system path protocol."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def __fspath__(self):
|
||||
"""Return the file system path representation of the object."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def __subclasshook__(cls, subclass):
|
||||
return hasattr(subclass, '__fspath__')
|
||||
42
Lib/_dummy_thread.py
vendored
42
Lib/_dummy_thread.py
vendored
@@ -14,8 +14,7 @@ Suggested usage is::
|
||||
# Exports only things specified by thread documentation;
|
||||
# skipping obsolete synonyms allocate(), start_new(), exit_thread().
|
||||
__all__ = ['error', 'start_new_thread', 'exit', 'get_ident', 'allocate_lock',
|
||||
'interrupt_main', 'LockType', 'RLock',
|
||||
'_count']
|
||||
'interrupt_main', 'LockType']
|
||||
|
||||
# A dummy value
|
||||
TIMEOUT_MAX = 2**31
|
||||
@@ -86,10 +85,6 @@ def _set_sentinel():
|
||||
"""Dummy implementation of _thread._set_sentinel()."""
|
||||
return LockType()
|
||||
|
||||
def _count():
|
||||
"""Dummy implementation of _thread._count()."""
|
||||
return 0
|
||||
|
||||
class LockType(object):
|
||||
"""Class implementing dummy implementation of _thread.LockType.
|
||||
|
||||
@@ -145,9 +140,6 @@ class LockType(object):
|
||||
def locked(self):
|
||||
return self.locked_status
|
||||
|
||||
def _at_fork_reinit(self):
|
||||
self.locked_status = False
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s.%s object at %s>" % (
|
||||
"locked" if self.locked_status else "unlocked",
|
||||
@@ -169,35 +161,3 @@ def interrupt_main():
|
||||
else:
|
||||
global _interrupt
|
||||
_interrupt = True
|
||||
|
||||
class RLock:
|
||||
def __init__(self):
|
||||
self.locked_count = 0
|
||||
|
||||
def acquire(self, waitflag=None, timeout=-1):
|
||||
self.locked_count += 1
|
||||
return True
|
||||
|
||||
__enter__ = acquire
|
||||
|
||||
def __exit__(self, typ, val, tb):
|
||||
self.release()
|
||||
|
||||
def release(self):
|
||||
if not self.locked_count:
|
||||
raise error
|
||||
self.locked_count -= 1
|
||||
return True
|
||||
|
||||
def locked(self):
|
||||
return self.locked_status != 0
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s.%s object owner=%s count=%s at %s>" % (
|
||||
"locked" if self.locked_count else "unlocked",
|
||||
self.__class__.__module__,
|
||||
self.__class__.__qualname__,
|
||||
get_ident() if self.locked_count else 0,
|
||||
self.locked_count,
|
||||
hex(id(self))
|
||||
)
|
||||
|
||||
71
Lib/_ios_support.py
vendored
71
Lib/_ios_support.py
vendored
@@ -1,71 +0,0 @@
|
||||
import sys
|
||||
try:
|
||||
from ctypes import cdll, c_void_p, c_char_p, util
|
||||
except ImportError:
|
||||
# ctypes is an optional module. If it's not present, we're limited in what
|
||||
# we can tell about the system, but we don't want to prevent the module
|
||||
# from working.
|
||||
print("ctypes isn't available; iOS system calls will not be available", file=sys.stderr)
|
||||
objc = None
|
||||
else:
|
||||
# ctypes is available. Load the ObjC library, and wrap the objc_getClass,
|
||||
# sel_registerName methods
|
||||
lib = util.find_library("objc")
|
||||
if lib is None:
|
||||
# Failed to load the objc library
|
||||
raise ImportError("ObjC runtime library couldn't be loaded")
|
||||
|
||||
objc = cdll.LoadLibrary(lib)
|
||||
objc.objc_getClass.restype = c_void_p
|
||||
objc.objc_getClass.argtypes = [c_char_p]
|
||||
objc.sel_registerName.restype = c_void_p
|
||||
objc.sel_registerName.argtypes = [c_char_p]
|
||||
|
||||
|
||||
def get_platform_ios():
|
||||
# Determine if this is a simulator using the multiarch value
|
||||
is_simulator = sys.implementation._multiarch.endswith("simulator")
|
||||
|
||||
# We can't use ctypes; abort
|
||||
if not objc:
|
||||
return None
|
||||
|
||||
# Most of the methods return ObjC objects
|
||||
objc.objc_msgSend.restype = c_void_p
|
||||
# All the methods used have no arguments.
|
||||
objc.objc_msgSend.argtypes = [c_void_p, c_void_p]
|
||||
|
||||
# Equivalent of:
|
||||
# device = [UIDevice currentDevice]
|
||||
UIDevice = objc.objc_getClass(b"UIDevice")
|
||||
SEL_currentDevice = objc.sel_registerName(b"currentDevice")
|
||||
device = objc.objc_msgSend(UIDevice, SEL_currentDevice)
|
||||
|
||||
# Equivalent of:
|
||||
# device_systemVersion = [device systemVersion]
|
||||
SEL_systemVersion = objc.sel_registerName(b"systemVersion")
|
||||
device_systemVersion = objc.objc_msgSend(device, SEL_systemVersion)
|
||||
|
||||
# Equivalent of:
|
||||
# device_systemName = [device systemName]
|
||||
SEL_systemName = objc.sel_registerName(b"systemName")
|
||||
device_systemName = objc.objc_msgSend(device, SEL_systemName)
|
||||
|
||||
# Equivalent of:
|
||||
# device_model = [device model]
|
||||
SEL_model = objc.sel_registerName(b"model")
|
||||
device_model = objc.objc_msgSend(device, SEL_model)
|
||||
|
||||
# UTF8String returns a const char*;
|
||||
SEL_UTF8String = objc.sel_registerName(b"UTF8String")
|
||||
objc.objc_msgSend.restype = c_char_p
|
||||
|
||||
# Equivalent of:
|
||||
# system = [device_systemName UTF8String]
|
||||
# release = [device_systemVersion UTF8String]
|
||||
# model = [device_model UTF8String]
|
||||
system = objc.objc_msgSend(device_systemName, SEL_UTF8String).decode()
|
||||
release = objc.objc_msgSend(device_systemVersion, SEL_UTF8String).decode()
|
||||
model = objc.objc_msgSend(device_model, SEL_UTF8String).decode()
|
||||
|
||||
return system, release, model, is_simulator
|
||||
35
Lib/_markupbase.py
vendored
35
Lib/_markupbase.py
vendored
@@ -29,6 +29,10 @@ class ParserBase:
|
||||
raise RuntimeError(
|
||||
"_markupbase.ParserBase must be subclassed")
|
||||
|
||||
def error(self, message):
|
||||
raise NotImplementedError(
|
||||
"subclasses of ParserBase must override error()")
|
||||
|
||||
def reset(self):
|
||||
self.lineno = 1
|
||||
self.offset = 0
|
||||
@@ -127,11 +131,12 @@ class ParserBase:
|
||||
# also in data attribute specifications of attlist declaration
|
||||
# also link type declaration subsets in linktype declarations
|
||||
# also link attribute specification lists in link declarations
|
||||
raise AssertionError("unsupported '[' char in %s declaration" % decltype)
|
||||
self.error("unsupported '[' char in %s declaration" % decltype)
|
||||
else:
|
||||
raise AssertionError("unexpected '[' char in declaration")
|
||||
self.error("unexpected '[' char in declaration")
|
||||
else:
|
||||
raise AssertionError("unexpected %r char in declaration" % rawdata[j])
|
||||
self.error(
|
||||
"unexpected %r char in declaration" % rawdata[j])
|
||||
if j < 0:
|
||||
return j
|
||||
return -1 # incomplete
|
||||
@@ -151,9 +156,7 @@ class ParserBase:
|
||||
# look for MS Office ]> ending
|
||||
match= _msmarkedsectionclose.search(rawdata, i+3)
|
||||
else:
|
||||
raise AssertionError(
|
||||
'unknown status keyword %r in marked section' % rawdata[i+3:j]
|
||||
)
|
||||
self.error('unknown status keyword %r in marked section' % rawdata[i+3:j])
|
||||
if not match:
|
||||
return -1
|
||||
if report:
|
||||
@@ -165,7 +168,7 @@ class ParserBase:
|
||||
def parse_comment(self, i, report=1):
|
||||
rawdata = self.rawdata
|
||||
if rawdata[i:i+4] != '<!--':
|
||||
raise AssertionError('unexpected call to parse_comment()')
|
||||
self.error('unexpected call to parse_comment()')
|
||||
match = _commentclose.search(rawdata, i+4)
|
||||
if not match:
|
||||
return -1
|
||||
@@ -189,9 +192,7 @@ class ParserBase:
|
||||
return -1
|
||||
if s != "<!":
|
||||
self.updatepos(declstartpos, j + 1)
|
||||
raise AssertionError(
|
||||
"unexpected char in internal subset (in %r)" % s
|
||||
)
|
||||
self.error("unexpected char in internal subset (in %r)" % s)
|
||||
if (j + 2) == n:
|
||||
# end of buffer; incomplete
|
||||
return -1
|
||||
@@ -208,9 +209,8 @@ class ParserBase:
|
||||
return -1
|
||||
if name not in {"attlist", "element", "entity", "notation"}:
|
||||
self.updatepos(declstartpos, j + 2)
|
||||
raise AssertionError(
|
||||
"unknown declaration %r in internal subset" % name
|
||||
)
|
||||
self.error(
|
||||
"unknown declaration %r in internal subset" % name)
|
||||
# handle the individual names
|
||||
meth = getattr(self, "_parse_doctype_" + name)
|
||||
j = meth(j, declstartpos)
|
||||
@@ -234,14 +234,14 @@ class ParserBase:
|
||||
if rawdata[j] == ">":
|
||||
return j
|
||||
self.updatepos(declstartpos, j)
|
||||
raise AssertionError("unexpected char after internal subset")
|
||||
self.error("unexpected char after internal subset")
|
||||
else:
|
||||
return -1
|
||||
elif c.isspace():
|
||||
j = j + 1
|
||||
else:
|
||||
self.updatepos(declstartpos, j)
|
||||
raise AssertionError("unexpected char %r in internal subset" % c)
|
||||
self.error("unexpected char %r in internal subset" % c)
|
||||
# end of buffer reached
|
||||
return -1
|
||||
|
||||
@@ -387,9 +387,8 @@ class ParserBase:
|
||||
return name.lower(), m.end()
|
||||
else:
|
||||
self.updatepos(declstartpos, i)
|
||||
raise AssertionError(
|
||||
"expected name token at %r" % rawdata[declstartpos:declstartpos+20]
|
||||
)
|
||||
self.error("expected name token at %r"
|
||||
% rawdata[declstartpos:declstartpos+20])
|
||||
|
||||
# To be overridden -- handlers for unknown objects
|
||||
def unknown_decl(self, data):
|
||||
|
||||
579
Lib/_osx_support.py
vendored
579
Lib/_osx_support.py
vendored
@@ -1,579 +0,0 @@
|
||||
"""Shared OS X support functions."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
__all__ = [
|
||||
'compiler_fixup',
|
||||
'customize_config_vars',
|
||||
'customize_compiler',
|
||||
'get_platform_osx',
|
||||
]
|
||||
|
||||
# configuration variables that may contain universal build flags,
|
||||
# like "-arch" or "-isdkroot", that may need customization for
|
||||
# the user environment
|
||||
_UNIVERSAL_CONFIG_VARS = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS', 'BASECFLAGS',
|
||||
'BLDSHARED', 'LDSHARED', 'CC', 'CXX',
|
||||
'PY_CFLAGS', 'PY_LDFLAGS', 'PY_CPPFLAGS',
|
||||
'PY_CORE_CFLAGS', 'PY_CORE_LDFLAGS')
|
||||
|
||||
# configuration variables that may contain compiler calls
|
||||
_COMPILER_CONFIG_VARS = ('BLDSHARED', 'LDSHARED', 'CC', 'CXX')
|
||||
|
||||
# prefix added to original configuration variable names
|
||||
_INITPRE = '_OSX_SUPPORT_INITIAL_'
|
||||
|
||||
|
||||
def _find_executable(executable, path=None):
|
||||
"""Tries to find 'executable' in the directories listed in 'path'.
|
||||
|
||||
A string listing directories separated by 'os.pathsep'; defaults to
|
||||
os.environ['PATH']. Returns the complete filename or None if not found.
|
||||
"""
|
||||
if path is None:
|
||||
path = os.environ['PATH']
|
||||
|
||||
paths = path.split(os.pathsep)
|
||||
base, ext = os.path.splitext(executable)
|
||||
|
||||
if (sys.platform == 'win32') and (ext != '.exe'):
|
||||
executable = executable + '.exe'
|
||||
|
||||
if not os.path.isfile(executable):
|
||||
for p in paths:
|
||||
f = os.path.join(p, executable)
|
||||
if os.path.isfile(f):
|
||||
# the file exists, we have a shot at spawn working
|
||||
return f
|
||||
return None
|
||||
else:
|
||||
return executable
|
||||
|
||||
|
||||
def _read_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.
|
||||
# tempfile is also not available then.
|
||||
import contextlib
|
||||
try:
|
||||
import tempfile
|
||||
fp = tempfile.NamedTemporaryFile()
|
||||
except ImportError:
|
||||
fp = open("/tmp/_osx_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().decode('utf-8').strip() if not os.system(cmd) else None
|
||||
|
||||
|
||||
def _find_build_tool(toolname):
|
||||
"""Find a build tool on current path or using xcrun"""
|
||||
return (_find_executable(toolname)
|
||||
or _read_output("/usr/bin/xcrun -find %s" % (toolname,))
|
||||
or ''
|
||||
)
|
||||
|
||||
_SYSTEM_VERSION = None
|
||||
|
||||
def _get_system_version():
|
||||
"""Return the OS X system version as a string"""
|
||||
# Reading this plist is a documented way to get the system
|
||||
# version (see the documentation for the Gestalt Manager)
|
||||
# We avoid using platform.mac_ver to avoid possible bootstrap issues during
|
||||
# the build of Python itself (distutils is used to build standard library
|
||||
# extensions).
|
||||
|
||||
global _SYSTEM_VERSION
|
||||
|
||||
if _SYSTEM_VERSION is None:
|
||||
_SYSTEM_VERSION = ''
|
||||
try:
|
||||
f = open('/System/Library/CoreServices/SystemVersion.plist', encoding="utf-8")
|
||||
except OSError:
|
||||
# We're on a plain darwin box, fall back to the default
|
||||
# behaviour.
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
m = re.search(r'<key>ProductUserVisibleVersion</key>\s*'
|
||||
r'<string>(.*?)</string>', f.read())
|
||||
finally:
|
||||
f.close()
|
||||
if m is not None:
|
||||
_SYSTEM_VERSION = '.'.join(m.group(1).split('.')[:2])
|
||||
# else: fall back to the default behaviour
|
||||
|
||||
return _SYSTEM_VERSION
|
||||
|
||||
_SYSTEM_VERSION_TUPLE = None
|
||||
def _get_system_version_tuple():
|
||||
"""
|
||||
Return the macOS system version as a tuple
|
||||
|
||||
The return value is safe to use to compare
|
||||
two version numbers.
|
||||
"""
|
||||
global _SYSTEM_VERSION_TUPLE
|
||||
if _SYSTEM_VERSION_TUPLE is None:
|
||||
osx_version = _get_system_version()
|
||||
if osx_version:
|
||||
try:
|
||||
_SYSTEM_VERSION_TUPLE = tuple(int(i) for i in osx_version.split('.'))
|
||||
except ValueError:
|
||||
_SYSTEM_VERSION_TUPLE = ()
|
||||
|
||||
return _SYSTEM_VERSION_TUPLE
|
||||
|
||||
|
||||
def _remove_original_values(_config_vars):
|
||||
"""Remove original unmodified values for testing"""
|
||||
# This is needed for higher-level cross-platform tests of get_platform.
|
||||
for k in list(_config_vars):
|
||||
if k.startswith(_INITPRE):
|
||||
del _config_vars[k]
|
||||
|
||||
def _save_modified_value(_config_vars, cv, newvalue):
|
||||
"""Save modified and original unmodified value of configuration var"""
|
||||
|
||||
oldvalue = _config_vars.get(cv, '')
|
||||
if (oldvalue != newvalue) and (_INITPRE + cv not in _config_vars):
|
||||
_config_vars[_INITPRE + cv] = oldvalue
|
||||
_config_vars[cv] = newvalue
|
||||
|
||||
|
||||
_cache_default_sysroot = None
|
||||
def _default_sysroot(cc):
|
||||
""" Returns the root of the default SDK for this system, or '/' """
|
||||
global _cache_default_sysroot
|
||||
|
||||
if _cache_default_sysroot is not None:
|
||||
return _cache_default_sysroot
|
||||
|
||||
contents = _read_output('%s -c -E -v - </dev/null' % (cc,), True)
|
||||
in_incdirs = False
|
||||
for line in contents.splitlines():
|
||||
if line.startswith("#include <...>"):
|
||||
in_incdirs = True
|
||||
elif line.startswith("End of search list"):
|
||||
in_incdirs = False
|
||||
elif in_incdirs:
|
||||
line = line.strip()
|
||||
if line == '/usr/include':
|
||||
_cache_default_sysroot = '/'
|
||||
elif line.endswith(".sdk/usr/include"):
|
||||
_cache_default_sysroot = line[:-12]
|
||||
if _cache_default_sysroot is None:
|
||||
_cache_default_sysroot = '/'
|
||||
|
||||
return _cache_default_sysroot
|
||||
|
||||
def _supports_universal_builds():
|
||||
"""Returns True if universal builds are supported on this system"""
|
||||
# As an approximation, we assume that if we are running on 10.4 or above,
|
||||
# then we are running with an Xcode environment that supports universal
|
||||
# builds, in particular -isysroot and -arch arguments to the compiler. This
|
||||
# is in support of allowing 10.4 universal builds to run on 10.3.x systems.
|
||||
|
||||
osx_version = _get_system_version_tuple()
|
||||
return bool(osx_version >= (10, 4)) if osx_version else False
|
||||
|
||||
def _supports_arm64_builds():
|
||||
"""Returns True if arm64 builds are supported on this system"""
|
||||
# There are two sets of systems supporting macOS/arm64 builds:
|
||||
# 1. macOS 11 and later, unconditionally
|
||||
# 2. macOS 10.15 with Xcode 12.2 or later
|
||||
# For now the second category is ignored.
|
||||
osx_version = _get_system_version_tuple()
|
||||
return osx_version >= (11, 0) if osx_version else False
|
||||
|
||||
|
||||
def _find_appropriate_compiler(_config_vars):
|
||||
"""Find appropriate C compiler for extension module builds"""
|
||||
|
||||
# Issue #13590:
|
||||
# The OSX location for the compiler varies between OSX
|
||||
# (or rather Xcode) releases. With older releases (up-to 10.5)
|
||||
# the compiler is in /usr/bin, with newer releases the compiler
|
||||
# can only be found inside Xcode.app if the "Command Line Tools"
|
||||
# are not installed.
|
||||
#
|
||||
# Furthermore, the compiler that can be used varies between
|
||||
# Xcode releases. Up to Xcode 4 it was possible to use 'gcc-4.2'
|
||||
# as the compiler, after that 'clang' should be used because
|
||||
# gcc-4.2 is either not present, or a copy of 'llvm-gcc' that
|
||||
# miscompiles Python.
|
||||
|
||||
# skip checks if the compiler was overridden with a CC env variable
|
||||
if 'CC' in os.environ:
|
||||
return _config_vars
|
||||
|
||||
# The CC config var might contain additional arguments.
|
||||
# Ignore them while searching.
|
||||
cc = oldcc = _config_vars['CC'].split()[0]
|
||||
if not _find_executable(cc):
|
||||
# Compiler is not found on the shell search PATH.
|
||||
# Now search for clang, first on PATH (if the Command LIne
|
||||
# Tools have been installed in / or if the user has provided
|
||||
# another location via CC). If not found, try using xcrun
|
||||
# to find an uninstalled clang (within a selected Xcode).
|
||||
|
||||
# NOTE: Cannot use subprocess here because of bootstrap
|
||||
# issues when building Python itself (and os.popen is
|
||||
# implemented on top of subprocess and is therefore not
|
||||
# usable as well)
|
||||
|
||||
cc = _find_build_tool('clang')
|
||||
|
||||
elif os.path.basename(cc).startswith('gcc'):
|
||||
# Compiler is GCC, check if it is LLVM-GCC
|
||||
data = _read_output("'%s' --version"
|
||||
% (cc.replace("'", "'\"'\"'"),))
|
||||
if data and 'llvm-gcc' in data:
|
||||
# Found LLVM-GCC, fall back to clang
|
||||
cc = _find_build_tool('clang')
|
||||
|
||||
if not cc:
|
||||
raise SystemError(
|
||||
"Cannot locate working compiler")
|
||||
|
||||
if cc != oldcc:
|
||||
# Found a replacement compiler.
|
||||
# Modify config vars using new compiler, if not already explicitly
|
||||
# overridden by an env variable, preserving additional arguments.
|
||||
for cv in _COMPILER_CONFIG_VARS:
|
||||
if cv in _config_vars and cv not in os.environ:
|
||||
cv_split = _config_vars[cv].split()
|
||||
cv_split[0] = cc if cv != 'CXX' else cc + '++'
|
||||
_save_modified_value(_config_vars, cv, ' '.join(cv_split))
|
||||
|
||||
return _config_vars
|
||||
|
||||
|
||||
def _remove_universal_flags(_config_vars):
|
||||
"""Remove all universal build arguments from config vars"""
|
||||
|
||||
for cv in _UNIVERSAL_CONFIG_VARS:
|
||||
# Do not alter a config var explicitly overridden by env var
|
||||
if cv in _config_vars and cv not in os.environ:
|
||||
flags = _config_vars[cv]
|
||||
flags = re.sub(r'-arch\s+\w+\s', ' ', flags, flags=re.ASCII)
|
||||
flags = re.sub(r'-isysroot\s*\S+', ' ', flags)
|
||||
_save_modified_value(_config_vars, cv, flags)
|
||||
|
||||
return _config_vars
|
||||
|
||||
|
||||
def _remove_unsupported_archs(_config_vars):
|
||||
"""Remove any unsupported archs from config vars"""
|
||||
# Different Xcode releases support different sets for '-arch'
|
||||
# flags. In particular, Xcode 4.x no longer supports the
|
||||
# PPC architectures.
|
||||
#
|
||||
# This code automatically removes '-arch ppc' and '-arch ppc64'
|
||||
# when these are not supported. That makes it possible to
|
||||
# build extensions on OSX 10.7 and later with the prebuilt
|
||||
# 32-bit installer on the python.org website.
|
||||
|
||||
# skip checks if the compiler was overridden with a CC env variable
|
||||
if 'CC' in os.environ:
|
||||
return _config_vars
|
||||
|
||||
if re.search(r'-arch\s+ppc', _config_vars['CFLAGS']) is not None:
|
||||
# NOTE: Cannot use subprocess here because of bootstrap
|
||||
# issues when building Python itself
|
||||
status = os.system(
|
||||
"""echo 'int main{};' | """
|
||||
"""'%s' -c -arch ppc -x c -o /dev/null /dev/null 2>/dev/null"""
|
||||
%(_config_vars['CC'].replace("'", "'\"'\"'"),))
|
||||
if status:
|
||||
# The compile failed for some reason. Because of differences
|
||||
# across Xcode and compiler versions, there is no reliable way
|
||||
# to be sure why it failed. Assume here it was due to lack of
|
||||
# PPC support and remove the related '-arch' flags from each
|
||||
# config variables not explicitly overridden by an environment
|
||||
# variable. If the error was for some other reason, we hope the
|
||||
# failure will show up again when trying to compile an extension
|
||||
# module.
|
||||
for cv in _UNIVERSAL_CONFIG_VARS:
|
||||
if cv in _config_vars and cv not in os.environ:
|
||||
flags = _config_vars[cv]
|
||||
flags = re.sub(r'-arch\s+ppc\w*\s', ' ', flags)
|
||||
_save_modified_value(_config_vars, cv, flags)
|
||||
|
||||
return _config_vars
|
||||
|
||||
|
||||
def _override_all_archs(_config_vars):
|
||||
"""Allow override of all archs with ARCHFLAGS env var"""
|
||||
# NOTE: This name was introduced by Apple in OSX 10.5 and
|
||||
# is used by several scripting languages distributed with
|
||||
# that OS release.
|
||||
if 'ARCHFLAGS' in os.environ:
|
||||
arch = os.environ['ARCHFLAGS']
|
||||
for cv in _UNIVERSAL_CONFIG_VARS:
|
||||
if cv in _config_vars and '-arch' in _config_vars[cv]:
|
||||
flags = _config_vars[cv]
|
||||
flags = re.sub(r'-arch\s+\w+\s', ' ', flags)
|
||||
flags = flags + ' ' + arch
|
||||
_save_modified_value(_config_vars, cv, flags)
|
||||
|
||||
return _config_vars
|
||||
|
||||
|
||||
def _check_for_unavailable_sdk(_config_vars):
|
||||
"""Remove references to any SDKs not available"""
|
||||
# If we're on OSX 10.5 or later and the user tries to
|
||||
# compile an extension using an SDK that is not present
|
||||
# on the current machine it is better to not use an SDK
|
||||
# than to fail. This is particularly important with
|
||||
# the standalone Command Line Tools alternative to a
|
||||
# full-blown Xcode install since the CLT packages do not
|
||||
# provide SDKs. If the SDK is not present, it is assumed
|
||||
# that the header files and dev libs have been installed
|
||||
# to /usr and /System/Library by either a standalone CLT
|
||||
# package or the CLT component within Xcode.
|
||||
cflags = _config_vars.get('CFLAGS', '')
|
||||
m = re.search(r'-isysroot\s*(\S+)', cflags)
|
||||
if m is not None:
|
||||
sdk = m.group(1)
|
||||
if not os.path.exists(sdk):
|
||||
for cv in _UNIVERSAL_CONFIG_VARS:
|
||||
# Do not alter a config var explicitly overridden by env var
|
||||
if cv in _config_vars and cv not in os.environ:
|
||||
flags = _config_vars[cv]
|
||||
flags = re.sub(r'-isysroot\s*\S+(?:\s|$)', ' ', flags)
|
||||
_save_modified_value(_config_vars, cv, flags)
|
||||
|
||||
return _config_vars
|
||||
|
||||
|
||||
def compiler_fixup(compiler_so, cc_args):
|
||||
"""
|
||||
This function will strip '-isysroot PATH' and '-arch ARCH' from the
|
||||
compile flags if the user has specified one them in extra_compile_flags.
|
||||
|
||||
This is needed because '-arch ARCH' adds another architecture to the
|
||||
build, without a way to remove an architecture. Furthermore GCC will
|
||||
barf if multiple '-isysroot' arguments are present.
|
||||
"""
|
||||
stripArch = stripSysroot = False
|
||||
|
||||
compiler_so = list(compiler_so)
|
||||
|
||||
if not _supports_universal_builds():
|
||||
# OSX before 10.4.0, these don't support -arch and -isysroot at
|
||||
# all.
|
||||
stripArch = stripSysroot = True
|
||||
else:
|
||||
stripArch = '-arch' in cc_args
|
||||
stripSysroot = any(arg for arg in cc_args if arg.startswith('-isysroot'))
|
||||
|
||||
if stripArch or 'ARCHFLAGS' in os.environ:
|
||||
while True:
|
||||
try:
|
||||
index = compiler_so.index('-arch')
|
||||
# Strip this argument and the next one:
|
||||
del compiler_so[index:index+2]
|
||||
except ValueError:
|
||||
break
|
||||
|
||||
elif not _supports_arm64_builds():
|
||||
# Look for "-arch arm64" and drop that
|
||||
for idx in reversed(range(len(compiler_so))):
|
||||
if compiler_so[idx] == '-arch' and compiler_so[idx+1] == "arm64":
|
||||
del compiler_so[idx:idx+2]
|
||||
|
||||
if 'ARCHFLAGS' in os.environ and not stripArch:
|
||||
# User specified different -arch flags in the environ,
|
||||
# see also distutils.sysconfig
|
||||
compiler_so = compiler_so + os.environ['ARCHFLAGS'].split()
|
||||
|
||||
if stripSysroot:
|
||||
while True:
|
||||
indices = [i for i,x in enumerate(compiler_so) if x.startswith('-isysroot')]
|
||||
if not indices:
|
||||
break
|
||||
index = indices[0]
|
||||
if compiler_so[index] == '-isysroot':
|
||||
# Strip this argument and the next one:
|
||||
del compiler_so[index:index+2]
|
||||
else:
|
||||
# It's '-isysroot/some/path' in one arg
|
||||
del compiler_so[index:index+1]
|
||||
|
||||
# Check if the SDK that is used during compilation actually exists,
|
||||
# the universal build requires the usage of a universal SDK and not all
|
||||
# users have that installed by default.
|
||||
sysroot = None
|
||||
argvar = cc_args
|
||||
indices = [i for i,x in enumerate(cc_args) if x.startswith('-isysroot')]
|
||||
if not indices:
|
||||
argvar = compiler_so
|
||||
indices = [i for i,x in enumerate(compiler_so) if x.startswith('-isysroot')]
|
||||
|
||||
for idx in indices:
|
||||
if argvar[idx] == '-isysroot':
|
||||
sysroot = argvar[idx+1]
|
||||
break
|
||||
else:
|
||||
sysroot = argvar[idx][len('-isysroot'):]
|
||||
break
|
||||
|
||||
if sysroot and not os.path.isdir(sysroot):
|
||||
sys.stderr.write(f"Compiling with an SDK that doesn't seem to exist: {sysroot}\n")
|
||||
sys.stderr.write("Please check your Xcode installation\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
return compiler_so
|
||||
|
||||
|
||||
def customize_config_vars(_config_vars):
|
||||
"""Customize Python build configuration variables.
|
||||
|
||||
Called internally from sysconfig with a mutable mapping
|
||||
containing name/value pairs parsed from the configured
|
||||
makefile used to build this interpreter. Returns
|
||||
the mapping updated as needed to reflect the environment
|
||||
in which the interpreter is running; in the case of
|
||||
a Python from a binary installer, the installed
|
||||
environment may be very different from the build
|
||||
environment, i.e. different OS levels, different
|
||||
built tools, different available CPU architectures.
|
||||
|
||||
This customization is performed whenever
|
||||
distutils.sysconfig.get_config_vars() is first
|
||||
called. It may be used in environments where no
|
||||
compilers are present, i.e. when installing pure
|
||||
Python dists. Customization of compiler paths
|
||||
and detection of unavailable archs is deferred
|
||||
until the first extension module build is
|
||||
requested (in distutils.sysconfig.customize_compiler).
|
||||
|
||||
Currently called from distutils.sysconfig
|
||||
"""
|
||||
|
||||
if not _supports_universal_builds():
|
||||
# On Mac OS X before 10.4, check if -arch and -isysroot
|
||||
# are in CFLAGS or LDFLAGS and remove them if they are.
|
||||
# This is needed when building extensions on a 10.3 system
|
||||
# using a universal build of python.
|
||||
_remove_universal_flags(_config_vars)
|
||||
|
||||
# Allow user to override all archs with ARCHFLAGS env var
|
||||
_override_all_archs(_config_vars)
|
||||
|
||||
# Remove references to sdks that are not found
|
||||
_check_for_unavailable_sdk(_config_vars)
|
||||
|
||||
return _config_vars
|
||||
|
||||
|
||||
def customize_compiler(_config_vars):
|
||||
"""Customize compiler path and configuration variables.
|
||||
|
||||
This customization is performed when the first
|
||||
extension module build is requested
|
||||
in distutils.sysconfig.customize_compiler.
|
||||
"""
|
||||
|
||||
# Find a compiler to use for extension module builds
|
||||
_find_appropriate_compiler(_config_vars)
|
||||
|
||||
# Remove ppc arch flags if not supported here
|
||||
_remove_unsupported_archs(_config_vars)
|
||||
|
||||
# Allow user to override all archs with ARCHFLAGS env var
|
||||
_override_all_archs(_config_vars)
|
||||
|
||||
return _config_vars
|
||||
|
||||
|
||||
def get_platform_osx(_config_vars, osname, release, machine):
|
||||
"""Filter values for get_platform()"""
|
||||
# called from get_platform() in sysconfig and distutils.util
|
||||
#
|
||||
# For our purposes, we'll assume that the system version from
|
||||
# distutils' perspective is what MACOSX_DEPLOYMENT_TARGET is set
|
||||
# to. This makes the compatibility story a bit more sane because the
|
||||
# machine is going to compile and link as if it were
|
||||
# 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
|
||||
|
||||
if macver:
|
||||
release = macver
|
||||
osname = "macosx"
|
||||
|
||||
# Use the original CFLAGS value, if available, so that we
|
||||
# return the same machine type for the platform string.
|
||||
# Otherwise, distutils may consider this a cross-compiling
|
||||
# case and disallow installs.
|
||||
cflags = _config_vars.get(_INITPRE+'CFLAGS',
|
||||
_config_vars.get('CFLAGS', ''))
|
||||
if macrelease:
|
||||
try:
|
||||
macrelease = tuple(int(i) for i in macrelease.split('.')[0:2])
|
||||
except ValueError:
|
||||
macrelease = (10, 3)
|
||||
else:
|
||||
# assume no universal support
|
||||
macrelease = (10, 3)
|
||||
|
||||
if (macrelease >= (10, 4)) and '-arch' in cflags.strip():
|
||||
# The universal build will build fat binaries, but not on
|
||||
# systems before 10.4
|
||||
|
||||
machine = 'fat'
|
||||
|
||||
archs = re.findall(r'-arch\s+(\S+)', cflags)
|
||||
archs = tuple(sorted(set(archs)))
|
||||
|
||||
if len(archs) == 1:
|
||||
machine = archs[0]
|
||||
elif archs == ('arm64', 'x86_64'):
|
||||
machine = 'universal2'
|
||||
elif archs == ('i386', 'ppc'):
|
||||
machine = 'fat'
|
||||
elif archs == ('i386', 'x86_64'):
|
||||
machine = 'intel'
|
||||
elif archs == ('i386', 'ppc', 'x86_64'):
|
||||
machine = 'fat3'
|
||||
elif archs == ('ppc64', 'x86_64'):
|
||||
machine = 'fat64'
|
||||
elif archs == ('i386', 'ppc', 'ppc64', 'x86_64'):
|
||||
machine = 'universal'
|
||||
else:
|
||||
raise ValueError(
|
||||
"Don't know machine value for archs=%r" % (archs,))
|
||||
|
||||
elif machine == 'i386':
|
||||
# On OSX the machine type returned by uname is always the
|
||||
# 32-bit variant, even if the executable architecture is
|
||||
# the 64-bit variant
|
||||
if sys.maxsize >= 2**32:
|
||||
machine = 'x86_64'
|
||||
|
||||
elif machine in ('PowerPC', 'Power_Macintosh'):
|
||||
# Pick a sane name for the PPC architecture.
|
||||
# See 'i386' case
|
||||
if sys.maxsize >= 2**32:
|
||||
machine = 'ppc64'
|
||||
else:
|
||||
machine = 'ppc'
|
||||
|
||||
return (osname, release, machine)
|
||||
6
Lib/_py_abc.py
vendored
6
Lib/_py_abc.py
vendored
@@ -32,8 +32,8 @@ class ABCMeta(type):
|
||||
# external code.
|
||||
_abc_invalidation_counter = 0
|
||||
|
||||
def __new__(mcls, name, bases, namespace, /, **kwargs):
|
||||
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||
cls = type.__new__(mcls, name, bases, namespace, **kwargs)
|
||||
# Compute set of abstract method names
|
||||
abstracts = {name
|
||||
for name, value in namespace.items()
|
||||
@@ -43,7 +43,7 @@ class ABCMeta(type):
|
||||
value = getattr(cls, name, None)
|
||||
if getattr(value, "__isabstractmethod__", False):
|
||||
abstracts.add(name)
|
||||
cls.__abstractmethods__ = frozenset(abstracts)
|
||||
cls.__abstractmethods__ = set(abstracts)
|
||||
# Set up inheritance registry
|
||||
cls._abc_registry = WeakSet()
|
||||
cls._abc_cache = WeakSet()
|
||||
|
||||
2643
Lib/_pydatetime.py
vendored
2643
Lib/_pydatetime.py
vendored
File diff suppressed because it is too large
Load Diff
286
Lib/_pydecimal.py
vendored
286
Lib/_pydecimal.py
vendored
@@ -13,7 +13,104 @@
|
||||
# bug) and will be backported. At this point the spec is stabilizing
|
||||
# and the updates are becoming fewer, smaller, and less significant.
|
||||
|
||||
"""Python decimal arithmetic module"""
|
||||
"""
|
||||
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
|
||||
>>>
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
# Two major classes
|
||||
@@ -43,11 +140,8 @@ __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 (deprecated, now always true)
|
||||
'HAVE_THREADS',
|
||||
|
||||
# C version: compile time choice that enables the coroutine local context
|
||||
'HAVE_CONTEXTVAR'
|
||||
# C version: compile time choice that enables the thread local context
|
||||
'HAVE_THREADS'
|
||||
]
|
||||
|
||||
__xname__ = __name__ # sys.modules lookup (--without-threads)
|
||||
@@ -62,7 +156,7 @@ import sys
|
||||
|
||||
try:
|
||||
from collections import namedtuple as _namedtuple
|
||||
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent', module='decimal')
|
||||
DecimalTuple = _namedtuple('DecimalTuple', 'sign digits exponent')
|
||||
except ImportError:
|
||||
DecimalTuple = lambda *args: args
|
||||
|
||||
@@ -78,7 +172,6 @@ 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
|
||||
@@ -97,7 +190,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.
|
||||
|
||||
@@ -145,7 +238,7 @@ class InvalidOperation(DecimalException):
|
||||
x ** (+-)INF
|
||||
An operand is invalid
|
||||
|
||||
The result of the operation after this is a quiet positive NaN,
|
||||
The result of the operation after these 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.
|
||||
@@ -338,40 +431,82 @@ _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.
|
||||
# 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.
|
||||
|
||||
import contextvars
|
||||
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
|
||||
|
||||
_current_context_var = contextvars.ContextVar('decimal_context')
|
||||
try:
|
||||
threading.local
|
||||
|
||||
_context_attributes = frozenset(
|
||||
['prec', 'Emin', 'Emax', 'capitals', 'clamp', 'rounding', 'flags', 'traps']
|
||||
)
|
||||
except AttributeError:
|
||||
|
||||
def getcontext():
|
||||
"""Returns this thread's context.
|
||||
# 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__
|
||||
|
||||
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 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
|
||||
|
||||
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)
|
||||
def getcontext():
|
||||
"""Returns this thread's context.
|
||||
|
||||
del contextvars # Don't contaminate the namespace
|
||||
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
|
||||
|
||||
def localcontext(ctx=None, **kwargs):
|
||||
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):
|
||||
"""Return a context manager for a copy of the supplied context
|
||||
|
||||
Uses a copy of the current context if no context is specified
|
||||
@@ -407,14 +542,8 @@ def localcontext(ctx=None, **kwargs):
|
||||
>>> print(getcontext().prec)
|
||||
28
|
||||
"""
|
||||
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
|
||||
if ctx is None: ctx = getcontext()
|
||||
return _ContextManager(ctx)
|
||||
|
||||
|
||||
##### Decimal class #######################################################
|
||||
@@ -424,7 +553,7 @@ def localcontext(ctx=None, **kwargs):
|
||||
# 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
|
||||
@@ -605,23 +734,18 @@ class Decimal(object):
|
||||
|
||||
"""
|
||||
if isinstance(f, int): # handle integer inputs
|
||||
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:
|
||||
return cls(f)
|
||||
if not isinstance(f, float):
|
||||
raise TypeError("argument must be int or float.")
|
||||
|
||||
result = _dec_from_triple(sign, coeff, -k)
|
||||
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
|
||||
result = _dec_from_triple(sign, str(n*5**k), -k)
|
||||
if cls is Decimal:
|
||||
return result
|
||||
else:
|
||||
@@ -864,7 +988,7 @@ class Decimal(object):
|
||||
if self.is_snan():
|
||||
raise TypeError('Cannot hash a signaling NaN value.')
|
||||
elif self.is_nan():
|
||||
return object.__hash__(self)
|
||||
return _PyHASH_NAN
|
||||
else:
|
||||
if self._sign:
|
||||
return -_PyHASH_INF
|
||||
@@ -1545,13 +1669,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
|
||||
@@ -2131,16 +2255,10 @@ class Decimal(object):
|
||||
else:
|
||||
return None
|
||||
|
||||
# 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:
|
||||
if xc >= 10**p:
|
||||
return None
|
||||
xe = -e-xe
|
||||
return _dec_from_triple(0, strxc, xe)
|
||||
return _dec_from_triple(0, str(xc), xe)
|
||||
|
||||
# now y is positive; find m and n such that y = m/n
|
||||
if ye >= 0:
|
||||
@@ -2149,7 +2267,7 @@ class Decimal(object):
|
||||
if xe != 0 and len(str(abs(yc*xe))) <= -ye:
|
||||
return None
|
||||
xc_bits = _nbits(xc)
|
||||
if len(str(abs(yc)*xc_bits)) <= -ye:
|
||||
if xc != 1 and len(str(abs(yc)*xc_bits)) <= -ye:
|
||||
return None
|
||||
m, n = yc, 10**(-ye)
|
||||
while m % 2 == n % 2 == 0:
|
||||
@@ -2162,7 +2280,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_bits <= n:
|
||||
if xc != 1 and xc_bits <= n:
|
||||
return None
|
||||
|
||||
xe, rem = divmod(xe, n)
|
||||
@@ -2190,18 +2308,13 @@ class Decimal(object):
|
||||
return None
|
||||
xc = xc**m
|
||||
xe *= m
|
||||
# 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:
|
||||
if xc > 10**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))
|
||||
@@ -3719,10 +3832,6 @@ 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)
|
||||
@@ -3753,7 +3862,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(adjusted_sign, intpart, fracpart, exp, spec)
|
||||
return _format_number(self._sign, intpart, fracpart, exp, spec)
|
||||
|
||||
def _dec_from_triple(sign, coefficient, exponent, special=False):
|
||||
"""Create a decimal instance directly, without any validation,
|
||||
@@ -5563,6 +5672,8 @@ class _WorkRep(object):
|
||||
def __repr__(self):
|
||||
return "(%r, %r, %r)" % (self.sign, self.int, self.exp)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
|
||||
def _normalize(op1, op2, prec = 0):
|
||||
@@ -6071,7 +6182,7 @@ _exact_half = re.compile('50*$').match
|
||||
#
|
||||
# A format specifier for Decimal looks like:
|
||||
#
|
||||
# [[fill]align][sign][z][#][0][minimumwidth][,][.precision][type]
|
||||
# [[fill]align][sign][#][0][minimumwidth][,][.precision][type]
|
||||
|
||||
_parse_format_specifier_regex = re.compile(r"""\A
|
||||
(?:
|
||||
@@ -6079,7 +6190,6 @@ _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+)?
|
||||
|
||||
2701
Lib/_pyio.py
vendored
2701
Lib/_pyio.py
vendored
File diff suppressed because it is too large
Load Diff
363
Lib/_pylong.py
vendored
363
Lib/_pylong.py
vendored
@@ -1,363 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,19 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,68 +0,0 @@
|
||||
"""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
|
||||
@@ -1,74 +0,0 @@
|
||||
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
|
||||
@@ -1,489 +0,0 @@
|
||||
# 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
|
||||
@@ -1,295 +0,0 @@
|
||||
# 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 []
|
||||
@@ -1,213 +0,0 @@
|
||||
# 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
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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
|
||||
@@ -1,76 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,419 +0,0 @@
|
||||
# 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
|
||||
@@ -1,114 +0,0 @@
|
||||
# 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
|
||||
@@ -1,213 +0,0 @@
|
||||
# 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
|
||||
@@ -1,59 +0,0 @@
|
||||
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)
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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
|
||||
@@ -1,175 +0,0 @@
|
||||
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 + '"')
|
||||
@@ -1,816 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,598 +0,0 @@
|
||||
# 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
|
||||
@@ -1,167 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,21 +0,0 @@
|
||||
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()
|
||||
@@ -1,8 +0,0 @@
|
||||
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]
|
||||
@@ -1,810 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,152 +0,0 @@
|
||||
# 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
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
@@ -1,618 +0,0 @@
|
||||
# Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com>
|
||||
#
|
||||
# All Rights Reserved
|
||||
#
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose is hereby granted without fee,
|
||||
# provided that the above copyright notice appear in all copies and
|
||||
# that both that copyright notice and this permission notice appear in
|
||||
# supporting documentation.
|
||||
#
|
||||
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
||||
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import msvcrt
|
||||
|
||||
from collections import deque
|
||||
import ctypes
|
||||
from ctypes.wintypes import (
|
||||
_COORD,
|
||||
WORD,
|
||||
SMALL_RECT,
|
||||
BOOL,
|
||||
HANDLE,
|
||||
CHAR,
|
||||
DWORD,
|
||||
WCHAR,
|
||||
SHORT,
|
||||
)
|
||||
from ctypes import Structure, POINTER, Union
|
||||
from .console import Event, Console
|
||||
from .trace import trace
|
||||
from .utils import wlen
|
||||
|
||||
try:
|
||||
from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined]
|
||||
except:
|
||||
# Keep MyPy happy off Windows
|
||||
from ctypes import CDLL as WinDLL, cdll as windll
|
||||
|
||||
def GetLastError() -> int:
|
||||
return 42
|
||||
|
||||
class WinError(OSError): # type: ignore[no-redef]
|
||||
def __init__(self, err: int | None, descr: str | None = None) -> None:
|
||||
self.err = err
|
||||
self.descr = descr
|
||||
|
||||
|
||||
TYPE_CHECKING = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import IO
|
||||
|
||||
# Virtual-Key Codes: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
VK_MAP: dict[int, str] = {
|
||||
0x23: "end", # VK_END
|
||||
0x24: "home", # VK_HOME
|
||||
0x25: "left", # VK_LEFT
|
||||
0x26: "up", # VK_UP
|
||||
0x27: "right", # VK_RIGHT
|
||||
0x28: "down", # VK_DOWN
|
||||
0x2E: "delete", # VK_DELETE
|
||||
0x70: "f1", # VK_F1
|
||||
0x71: "f2", # VK_F2
|
||||
0x72: "f3", # VK_F3
|
||||
0x73: "f4", # VK_F4
|
||||
0x74: "f5", # VK_F5
|
||||
0x75: "f6", # VK_F6
|
||||
0x76: "f7", # VK_F7
|
||||
0x77: "f8", # VK_F8
|
||||
0x78: "f9", # VK_F9
|
||||
0x79: "f10", # VK_F10
|
||||
0x7A: "f11", # VK_F11
|
||||
0x7B: "f12", # VK_F12
|
||||
0x7C: "f13", # VK_F13
|
||||
0x7D: "f14", # VK_F14
|
||||
0x7E: "f15", # VK_F15
|
||||
0x7F: "f16", # VK_F16
|
||||
0x80: "f17", # VK_F17
|
||||
0x81: "f18", # VK_F18
|
||||
0x82: "f19", # VK_F19
|
||||
0x83: "f20", # VK_F20
|
||||
}
|
||||
|
||||
# Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
|
||||
ERASE_IN_LINE = "\x1b[K"
|
||||
MOVE_LEFT = "\x1b[{}D"
|
||||
MOVE_RIGHT = "\x1b[{}C"
|
||||
MOVE_UP = "\x1b[{}A"
|
||||
MOVE_DOWN = "\x1b[{}B"
|
||||
CLEAR = "\x1b[H\x1b[J"
|
||||
|
||||
|
||||
class _error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WindowsConsole(Console):
|
||||
def __init__(
|
||||
self,
|
||||
f_in: IO[bytes] | int = 0,
|
||||
f_out: IO[bytes] | int = 1,
|
||||
term: str = "",
|
||||
encoding: str = "",
|
||||
):
|
||||
super().__init__(f_in, f_out, term, encoding)
|
||||
|
||||
SetConsoleMode(
|
||||
OutHandle,
|
||||
ENABLE_WRAP_AT_EOL_OUTPUT
|
||||
| ENABLE_PROCESSED_OUTPUT
|
||||
| ENABLE_VIRTUAL_TERMINAL_PROCESSING,
|
||||
)
|
||||
self.screen: list[str] = []
|
||||
self.width = 80
|
||||
self.height = 25
|
||||
self.__offset = 0
|
||||
self.event_queue: deque[Event] = deque()
|
||||
try:
|
||||
self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined]
|
||||
except ValueError:
|
||||
# Console I/O is redirected, fallback...
|
||||
self.out = None
|
||||
|
||||
def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None:
|
||||
"""
|
||||
Refresh the console screen.
|
||||
|
||||
Parameters:
|
||||
- screen (list): List of strings representing the screen contents.
|
||||
- c_xy (tuple): Cursor position (x, y) on the screen.
|
||||
"""
|
||||
cx, cy = c_xy
|
||||
|
||||
while len(self.screen) < min(len(screen), self.height):
|
||||
self._hide_cursor()
|
||||
self._move_relative(0, len(self.screen) - 1)
|
||||
self.__write("\n")
|
||||
self.posxy = 0, len(self.screen)
|
||||
self.screen.append("")
|
||||
|
||||
px, py = self.posxy
|
||||
old_offset = offset = self.__offset
|
||||
height = self.height
|
||||
|
||||
# we make sure the cursor is on the screen, and that we're
|
||||
# using all of the screen if we can
|
||||
if cy < offset:
|
||||
offset = cy
|
||||
elif cy >= offset + height:
|
||||
offset = cy - height + 1
|
||||
scroll_lines = offset - old_offset
|
||||
|
||||
# Scrolling the buffer as the current input is greater than the visible
|
||||
# portion of the window. We need to scroll the visible portion and the
|
||||
# entire history
|
||||
self._scroll(scroll_lines, self._getscrollbacksize())
|
||||
self.posxy = self.posxy[0], self.posxy[1] + scroll_lines
|
||||
self.__offset += scroll_lines
|
||||
|
||||
for i in range(scroll_lines):
|
||||
self.screen.append("")
|
||||
elif offset > 0 and len(screen) < offset + height:
|
||||
offset = max(len(screen) - height, 0)
|
||||
screen.append("")
|
||||
|
||||
oldscr = self.screen[old_offset : old_offset + height]
|
||||
newscr = screen[offset : offset + height]
|
||||
|
||||
self.__offset = offset
|
||||
|
||||
self._hide_cursor()
|
||||
for (
|
||||
y,
|
||||
oldline,
|
||||
newline,
|
||||
) in zip(range(offset, offset + height), oldscr, newscr):
|
||||
if oldline != newline:
|
||||
self.__write_changed_line(y, oldline, newline, px)
|
||||
|
||||
y = len(newscr)
|
||||
while y < len(oldscr):
|
||||
self._move_relative(0, y)
|
||||
self.posxy = 0, y
|
||||
self._erase_to_end()
|
||||
y += 1
|
||||
|
||||
self._show_cursor()
|
||||
|
||||
self.screen = screen
|
||||
self.move_cursor(cx, cy)
|
||||
|
||||
@property
|
||||
def input_hook(self):
|
||||
try:
|
||||
import nt
|
||||
except ImportError:
|
||||
return None
|
||||
if nt._is_inputhook_installed():
|
||||
return nt._inputhook
|
||||
|
||||
def __write_changed_line(
|
||||
self, y: int, oldline: str, newline: str, px_coord: int
|
||||
) -> None:
|
||||
# this is frustrating; there's no reason to test (say)
|
||||
# self.dch1 inside the loop -- but alternative ways of
|
||||
# structuring this function are equally painful (I'm trying to
|
||||
# avoid writing code generators these days...)
|
||||
minlen = min(wlen(oldline), wlen(newline))
|
||||
x_pos = 0
|
||||
x_coord = 0
|
||||
|
||||
px_pos = 0
|
||||
j = 0
|
||||
for c in oldline:
|
||||
if j >= px_coord:
|
||||
break
|
||||
j += wlen(c)
|
||||
px_pos += 1
|
||||
|
||||
# reuse the oldline as much as possible, but stop as soon as we
|
||||
# encounter an ESCAPE, because it might be the start of an escape
|
||||
# sequene
|
||||
while (
|
||||
x_coord < minlen
|
||||
and oldline[x_pos] == newline[x_pos]
|
||||
and newline[x_pos] != "\x1b"
|
||||
):
|
||||
x_coord += wlen(newline[x_pos])
|
||||
x_pos += 1
|
||||
|
||||
self._hide_cursor()
|
||||
self._move_relative(x_coord, y)
|
||||
if wlen(oldline) > wlen(newline):
|
||||
self._erase_to_end()
|
||||
|
||||
self.__write(newline[x_pos:])
|
||||
if wlen(newline) == self.width:
|
||||
# If we wrapped we want to start at the next line
|
||||
self._move_relative(0, y + 1)
|
||||
self.posxy = 0, y + 1
|
||||
else:
|
||||
self.posxy = wlen(newline), y
|
||||
|
||||
if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline:
|
||||
# ANSI escape characters are present, so we can't assume
|
||||
# anything about the position of the cursor. Moving the cursor
|
||||
# to the left margin should work to get to a known position.
|
||||
self.move_cursor(0, y)
|
||||
|
||||
def _scroll(
|
||||
self, top: int, bottom: int, left: int | None = None, right: int | None = None
|
||||
) -> None:
|
||||
scroll_rect = SMALL_RECT()
|
||||
scroll_rect.Top = SHORT(top)
|
||||
scroll_rect.Bottom = SHORT(bottom)
|
||||
scroll_rect.Left = SHORT(0 if left is None else left)
|
||||
scroll_rect.Right = SHORT(
|
||||
self.getheightwidth()[1] - 1 if right is None else right
|
||||
)
|
||||
destination_origin = _COORD()
|
||||
fill_info = CHAR_INFO()
|
||||
fill_info.UnicodeChar = " "
|
||||
|
||||
if not ScrollConsoleScreenBuffer(
|
||||
OutHandle, scroll_rect, None, destination_origin, fill_info
|
||||
):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
def _hide_cursor(self):
|
||||
self.__write("\x1b[?25l")
|
||||
|
||||
def _show_cursor(self):
|
||||
self.__write("\x1b[?25h")
|
||||
|
||||
def _enable_blinking(self):
|
||||
self.__write("\x1b[?12h")
|
||||
|
||||
def _disable_blinking(self):
|
||||
self.__write("\x1b[?12l")
|
||||
|
||||
def __write(self, text: str) -> None:
|
||||
if "\x1a" in text:
|
||||
text = ''.join(["^Z" if x == '\x1a' else x for x in text])
|
||||
|
||||
if self.out is not None:
|
||||
self.out.write(text.encode(self.encoding, "replace"))
|
||||
self.out.flush()
|
||||
else:
|
||||
os.write(self.output_fd, text.encode(self.encoding, "replace"))
|
||||
|
||||
@property
|
||||
def screen_xy(self) -> tuple[int, int]:
|
||||
info = CONSOLE_SCREEN_BUFFER_INFO()
|
||||
if not GetConsoleScreenBufferInfo(OutHandle, info):
|
||||
raise WinError(GetLastError())
|
||||
return info.dwCursorPosition.X, info.dwCursorPosition.Y
|
||||
|
||||
def _erase_to_end(self) -> None:
|
||||
self.__write(ERASE_IN_LINE)
|
||||
|
||||
def prepare(self) -> None:
|
||||
trace("prepare")
|
||||
self.screen = []
|
||||
self.height, self.width = self.getheightwidth()
|
||||
|
||||
self.posxy = 0, 0
|
||||
self.__gone_tall = 0
|
||||
self.__offset = 0
|
||||
|
||||
def restore(self) -> None:
|
||||
pass
|
||||
|
||||
def _move_relative(self, x: int, y: int) -> None:
|
||||
"""Moves relative to the current posxy"""
|
||||
dx = x - self.posxy[0]
|
||||
dy = y - self.posxy[1]
|
||||
if dx < 0:
|
||||
self.__write(MOVE_LEFT.format(-dx))
|
||||
elif dx > 0:
|
||||
self.__write(MOVE_RIGHT.format(dx))
|
||||
|
||||
if dy < 0:
|
||||
self.__write(MOVE_UP.format(-dy))
|
||||
elif dy > 0:
|
||||
self.__write(MOVE_DOWN.format(dy))
|
||||
|
||||
def move_cursor(self, x: int, y: int) -> None:
|
||||
if x < 0 or y < 0:
|
||||
raise ValueError(f"Bad cursor position {x}, {y}")
|
||||
|
||||
if y < self.__offset or y >= self.__offset + self.height:
|
||||
self.event_queue.insert(0, Event("scroll", ""))
|
||||
else:
|
||||
self._move_relative(x, y)
|
||||
self.posxy = x, y
|
||||
|
||||
def set_cursor_vis(self, visible: bool) -> None:
|
||||
if visible:
|
||||
self._show_cursor()
|
||||
else:
|
||||
self._hide_cursor()
|
||||
|
||||
def getheightwidth(self) -> tuple[int, int]:
|
||||
"""Return (height, width) where height and width are the height
|
||||
and width of the terminal window in characters."""
|
||||
info = CONSOLE_SCREEN_BUFFER_INFO()
|
||||
if not GetConsoleScreenBufferInfo(OutHandle, info):
|
||||
raise WinError(GetLastError())
|
||||
return (
|
||||
info.srWindow.Bottom - info.srWindow.Top + 1,
|
||||
info.srWindow.Right - info.srWindow.Left + 1,
|
||||
)
|
||||
|
||||
def _getscrollbacksize(self) -> int:
|
||||
info = CONSOLE_SCREEN_BUFFER_INFO()
|
||||
if not GetConsoleScreenBufferInfo(OutHandle, info):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
return info.srWindow.Bottom # type: ignore[no-any-return]
|
||||
|
||||
def _read_input(self, block: bool = True) -> INPUT_RECORD | None:
|
||||
if not block:
|
||||
events = DWORD()
|
||||
if not GetNumberOfConsoleInputEvents(InHandle, events):
|
||||
raise WinError(GetLastError())
|
||||
if not events.value:
|
||||
return None
|
||||
|
||||
rec = INPUT_RECORD()
|
||||
read = DWORD()
|
||||
if not ReadConsoleInput(InHandle, rec, 1, read):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
return rec
|
||||
|
||||
def get_event(self, block: bool = True) -> Event | None:
|
||||
"""Return an Event instance. Returns None if |block| is false
|
||||
and there is no event pending, otherwise waits for the
|
||||
completion of an event."""
|
||||
if self.event_queue:
|
||||
return self.event_queue.pop()
|
||||
|
||||
while True:
|
||||
rec = self._read_input(block)
|
||||
if rec is None:
|
||||
return None
|
||||
|
||||
if rec.EventType == WINDOW_BUFFER_SIZE_EVENT:
|
||||
return Event("resize", "")
|
||||
|
||||
if rec.EventType != KEY_EVENT or not rec.Event.KeyEvent.bKeyDown:
|
||||
# Only process keys and keydown events
|
||||
if block:
|
||||
continue
|
||||
return None
|
||||
|
||||
key = rec.Event.KeyEvent.uChar.UnicodeChar
|
||||
|
||||
if rec.Event.KeyEvent.uChar.UnicodeChar == "\r":
|
||||
# Make enter make unix-like
|
||||
return Event(evt="key", data="\n", raw=b"\n")
|
||||
elif rec.Event.KeyEvent.wVirtualKeyCode == 8:
|
||||
# Turn backspace directly into the command
|
||||
return Event(
|
||||
evt="key",
|
||||
data="backspace",
|
||||
raw=rec.Event.KeyEvent.uChar.UnicodeChar,
|
||||
)
|
||||
elif rec.Event.KeyEvent.uChar.UnicodeChar == "\x00":
|
||||
# Handle special keys like arrow keys and translate them into the appropriate command
|
||||
code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode)
|
||||
if code:
|
||||
return Event(
|
||||
evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar
|
||||
)
|
||||
if block:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar)
|
||||
|
||||
def push_char(self, char: int | bytes) -> None:
|
||||
"""
|
||||
Push a character to the console event queue.
|
||||
"""
|
||||
raise NotImplementedError("push_char not supported on Windows")
|
||||
|
||||
def beep(self) -> None:
|
||||
self.__write("\x07")
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Wipe the screen"""
|
||||
self.__write(CLEAR)
|
||||
self.posxy = 0, 0
|
||||
self.screen = [""]
|
||||
|
||||
def finish(self) -> None:
|
||||
"""Move the cursor to the end of the display and otherwise get
|
||||
ready for end. XXX could be merged with restore? Hmm."""
|
||||
y = len(self.screen) - 1
|
||||
while y >= 0 and not self.screen[y]:
|
||||
y -= 1
|
||||
self._move_relative(0, min(y, self.height + self.__offset - 1))
|
||||
self.__write("\r\n")
|
||||
|
||||
def flushoutput(self) -> None:
|
||||
"""Flush all output to the screen (assuming there's some
|
||||
buffering going on somewhere).
|
||||
|
||||
All output on Windows is unbuffered so this is a nop"""
|
||||
pass
|
||||
|
||||
def forgetinput(self) -> None:
|
||||
"""Forget all pending, but not yet processed input."""
|
||||
if not FlushConsoleInputBuffer(InHandle):
|
||||
raise WinError(GetLastError())
|
||||
|
||||
def getpending(self) -> Event:
|
||||
"""Return the characters that have been typed but not yet
|
||||
processed."""
|
||||
return Event("key", "", b"")
|
||||
|
||||
def wait(self, timeout: float | None) -> bool:
|
||||
"""Wait for an event."""
|
||||
# Poor man's Windows select loop
|
||||
start_time = time.time()
|
||||
while True:
|
||||
if msvcrt.kbhit(): # type: ignore[attr-defined]
|
||||
return True
|
||||
if timeout and time.time() - start_time > timeout / 1000:
|
||||
return False
|
||||
time.sleep(0.01)
|
||||
|
||||
def repaint(self) -> None:
|
||||
raise NotImplementedError("No repaint support")
|
||||
|
||||
|
||||
# Windows interop
|
||||
class CONSOLE_SCREEN_BUFFER_INFO(Structure):
|
||||
_fields_ = [
|
||||
("dwSize", _COORD),
|
||||
("dwCursorPosition", _COORD),
|
||||
("wAttributes", WORD),
|
||||
("srWindow", SMALL_RECT),
|
||||
("dwMaximumWindowSize", _COORD),
|
||||
]
|
||||
|
||||
|
||||
class CONSOLE_CURSOR_INFO(Structure):
|
||||
_fields_ = [
|
||||
("dwSize", DWORD),
|
||||
("bVisible", BOOL),
|
||||
]
|
||||
|
||||
|
||||
class CHAR_INFO(Structure):
|
||||
_fields_ = [
|
||||
("UnicodeChar", WCHAR),
|
||||
("Attributes", WORD),
|
||||
]
|
||||
|
||||
|
||||
class Char(Union):
|
||||
_fields_ = [
|
||||
("UnicodeChar", WCHAR),
|
||||
("Char", CHAR),
|
||||
]
|
||||
|
||||
|
||||
class KeyEvent(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("bKeyDown", BOOL),
|
||||
("wRepeatCount", WORD),
|
||||
("wVirtualKeyCode", WORD),
|
||||
("wVirtualScanCode", WORD),
|
||||
("uChar", Char),
|
||||
("dwControlKeyState", DWORD),
|
||||
]
|
||||
|
||||
|
||||
class WindowsBufferSizeEvent(ctypes.Structure):
|
||||
_fields_ = [("dwSize", _COORD)]
|
||||
|
||||
|
||||
class ConsoleEvent(ctypes.Union):
|
||||
_fields_ = [
|
||||
("KeyEvent", KeyEvent),
|
||||
("WindowsBufferSizeEvent", WindowsBufferSizeEvent),
|
||||
]
|
||||
|
||||
|
||||
class INPUT_RECORD(Structure):
|
||||
_fields_ = [("EventType", WORD), ("Event", ConsoleEvent)]
|
||||
|
||||
|
||||
KEY_EVENT = 0x01
|
||||
FOCUS_EVENT = 0x10
|
||||
MENU_EVENT = 0x08
|
||||
MOUSE_EVENT = 0x02
|
||||
WINDOW_BUFFER_SIZE_EVENT = 0x04
|
||||
|
||||
ENABLE_PROCESSED_OUTPUT = 0x01
|
||||
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
|
||||
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
|
||||
|
||||
STD_INPUT_HANDLE = -10
|
||||
STD_OUTPUT_HANDLE = -11
|
||||
|
||||
if sys.platform == "win32":
|
||||
_KERNEL32 = WinDLL("kernel32", use_last_error=True)
|
||||
|
||||
GetStdHandle = windll.kernel32.GetStdHandle
|
||||
GetStdHandle.argtypes = [DWORD]
|
||||
GetStdHandle.restype = HANDLE
|
||||
|
||||
GetConsoleScreenBufferInfo = _KERNEL32.GetConsoleScreenBufferInfo
|
||||
GetConsoleScreenBufferInfo.argtypes = [
|
||||
HANDLE,
|
||||
ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO),
|
||||
]
|
||||
GetConsoleScreenBufferInfo.restype = BOOL
|
||||
|
||||
ScrollConsoleScreenBuffer = _KERNEL32.ScrollConsoleScreenBufferW
|
||||
ScrollConsoleScreenBuffer.argtypes = [
|
||||
HANDLE,
|
||||
POINTER(SMALL_RECT),
|
||||
POINTER(SMALL_RECT),
|
||||
_COORD,
|
||||
POINTER(CHAR_INFO),
|
||||
]
|
||||
ScrollConsoleScreenBuffer.restype = BOOL
|
||||
|
||||
SetConsoleMode = _KERNEL32.SetConsoleMode
|
||||
SetConsoleMode.argtypes = [HANDLE, DWORD]
|
||||
SetConsoleMode.restype = BOOL
|
||||
|
||||
ReadConsoleInput = _KERNEL32.ReadConsoleInputW
|
||||
ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)]
|
||||
ReadConsoleInput.restype = BOOL
|
||||
|
||||
GetNumberOfConsoleInputEvents = _KERNEL32.GetNumberOfConsoleInputEvents
|
||||
GetNumberOfConsoleInputEvents.argtypes = [HANDLE, POINTER(DWORD)]
|
||||
GetNumberOfConsoleInputEvents.restype = BOOL
|
||||
|
||||
FlushConsoleInputBuffer = _KERNEL32.FlushConsoleInputBuffer
|
||||
FlushConsoleInputBuffer.argtypes = [HANDLE]
|
||||
FlushConsoleInputBuffer.restype = BOOL
|
||||
|
||||
OutHandle = GetStdHandle(STD_OUTPUT_HANDLE)
|
||||
InHandle = GetStdHandle(STD_INPUT_HANDLE)
|
||||
else:
|
||||
|
||||
def _win_only(*args, **kwargs):
|
||||
raise NotImplementedError("Windows only")
|
||||
|
||||
GetStdHandle = _win_only
|
||||
GetConsoleScreenBufferInfo = _win_only
|
||||
ScrollConsoleScreenBuffer = _win_only
|
||||
SetConsoleMode = _win_only
|
||||
ReadConsoleInput = _win_only
|
||||
GetNumberOfConsoleInputEvents = _win_only
|
||||
FlushConsoleInputBuffer = _win_only
|
||||
OutHandle = 0
|
||||
InHandle = 0
|
||||
3
Lib/_sitebuiltins.py
vendored
3
Lib/_sitebuiltins.py
vendored
@@ -10,6 +10,7 @@ The objects used by the site module to add custom builtins.
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
class Quitter(object):
|
||||
def __init__(self, name, eof):
|
||||
self.name = name
|
||||
@@ -47,7 +48,7 @@ class _Printer(object):
|
||||
data = None
|
||||
for filename in self.__filenames:
|
||||
try:
|
||||
with open(filename, encoding='utf-8') as fp:
|
||||
with open(filename, "r") as fp:
|
||||
data = fp.read()
|
||||
break
|
||||
except OSError:
|
||||
|
||||
1297
Lib/_sre.py
vendored
Normal file
1297
Lib/_sre.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
565
Lib/_strptime.py
vendored
565
Lib/_strptime.py
vendored
@@ -1,565 +0,0 @@
|
||||
"""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)
|
||||
131
Lib/_threading_local.py
vendored
131
Lib/_threading_local.py
vendored
@@ -4,6 +4,133 @@
|
||||
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
|
||||
@@ -67,6 +194,7 @@ class _localimpl:
|
||||
|
||||
@contextmanager
|
||||
def _patch(self):
|
||||
old = object.__getattribute__(self, '__dict__')
|
||||
impl = object.__getattribute__(self, '_local__impl')
|
||||
try:
|
||||
dct = impl.get_dict()
|
||||
@@ -77,12 +205,13 @@ 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)
|
||||
|
||||
16
Lib/_weakrefset.py
vendored
16
Lib/_weakrefset.py
vendored
@@ -3,7 +3,6 @@
|
||||
# by abc.py to load everything else at startup.
|
||||
|
||||
from _weakref import ref
|
||||
from types import GenericAlias
|
||||
|
||||
__all__ = ['WeakSet']
|
||||
|
||||
@@ -51,14 +50,10 @@ class WeakSet:
|
||||
self.update(data)
|
||||
|
||||
def _commit_removals(self):
|
||||
pop = self._pending_removals.pop
|
||||
l = self._pending_removals
|
||||
discard = self.data.discard
|
||||
while True:
|
||||
try:
|
||||
item = pop()
|
||||
except IndexError:
|
||||
return
|
||||
discard(item)
|
||||
while l:
|
||||
discard(l.pop())
|
||||
|
||||
def __iter__(self):
|
||||
with _IterationGuard(self):
|
||||
@@ -80,7 +75,8 @@ class WeakSet:
|
||||
return wr in self.data
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (list(self),), self.__getstate__()
|
||||
return (self.__class__, (list(self),),
|
||||
getattr(self, '__dict__', None))
|
||||
|
||||
def add(self, item):
|
||||
if self._pending_removals:
|
||||
@@ -201,5 +197,3 @@ class WeakSet:
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.data)
|
||||
|
||||
__class_getitem__ = classmethod(GenericAlias)
|
||||
|
||||
90
Lib/abc.py
vendored
90
Lib/abc.py
vendored
@@ -11,14 +11,13 @@ def abstractmethod(funcobj):
|
||||
class that has a metaclass derived from ABCMeta cannot be
|
||||
instantiated unless all of its abstract methods are overridden.
|
||||
The abstract methods can be called using any of the normal
|
||||
'super' call mechanisms. abstractmethod() may be used to declare
|
||||
abstract methods for properties and descriptors.
|
||||
'super' call mechanisms.
|
||||
|
||||
Usage:
|
||||
|
||||
class C(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def my_abstract_method(self, arg1, arg2, argN):
|
||||
def my_abstract_method(self, ...):
|
||||
...
|
||||
"""
|
||||
funcobj.__isabstractmethod__ = True
|
||||
@@ -28,14 +27,17 @@ def abstractmethod(funcobj):
|
||||
class abstractclassmethod(classmethod):
|
||||
"""A decorator indicating abstract classmethods.
|
||||
|
||||
Deprecated, use 'classmethod' with 'abstractmethod' instead:
|
||||
Similar to abstractmethod.
|
||||
|
||||
class C(ABC):
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
Usage:
|
||||
|
||||
class C(metaclass=ABCMeta):
|
||||
@abstractclassmethod
|
||||
def my_abstract_classmethod(cls, ...):
|
||||
...
|
||||
|
||||
'abstractclassmethod' is deprecated. Use 'classmethod' with
|
||||
'abstractmethod' instead.
|
||||
"""
|
||||
|
||||
__isabstractmethod__ = True
|
||||
@@ -48,14 +50,17 @@ class abstractclassmethod(classmethod):
|
||||
class abstractstaticmethod(staticmethod):
|
||||
"""A decorator indicating abstract staticmethods.
|
||||
|
||||
Deprecated, use 'staticmethod' with 'abstractmethod' instead:
|
||||
Similar to abstractmethod.
|
||||
|
||||
class C(ABC):
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
Usage:
|
||||
|
||||
class C(metaclass=ABCMeta):
|
||||
@abstractstaticmethod
|
||||
def my_abstract_staticmethod(...):
|
||||
...
|
||||
|
||||
'abstractstaticmethod' is deprecated. Use 'staticmethod' with
|
||||
'abstractmethod' instead.
|
||||
"""
|
||||
|
||||
__isabstractmethod__ = True
|
||||
@@ -68,14 +73,29 @@ class abstractstaticmethod(staticmethod):
|
||||
class abstractproperty(property):
|
||||
"""A decorator indicating abstract properties.
|
||||
|
||||
Deprecated, use 'property' with 'abstractmethod' instead:
|
||||
Requires that the metaclass is ABCMeta or derived from it. A
|
||||
class that has a metaclass derived from ABCMeta cannot be
|
||||
instantiated unless all of its abstract properties are overridden.
|
||||
The abstract properties can be called using any of the normal
|
||||
'super' call mechanisms.
|
||||
|
||||
class C(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
Usage:
|
||||
|
||||
class C(metaclass=ABCMeta):
|
||||
@abstractproperty
|
||||
def my_abstract_property(self):
|
||||
...
|
||||
|
||||
This defines a read-only property; you can also define a read-write
|
||||
abstract property using the 'long' form of property declaration:
|
||||
|
||||
class C(metaclass=ABCMeta):
|
||||
def getx(self): ...
|
||||
def setx(self, value): ...
|
||||
x = abstractproperty(getx, setx)
|
||||
|
||||
'abstractproperty' is deprecated. Use 'property' with 'abstractmethod'
|
||||
instead.
|
||||
"""
|
||||
|
||||
__isabstractmethod__ = True
|
||||
@@ -102,7 +122,7 @@ else:
|
||||
implementations defined by the registering ABC be callable (not
|
||||
even via super()).
|
||||
"""
|
||||
def __new__(mcls, name, bases, namespace, /, **kwargs):
|
||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
|
||||
_abc_init(cls)
|
||||
return cls
|
||||
@@ -143,44 +163,6 @@ else:
|
||||
_reset_caches(cls)
|
||||
|
||||
|
||||
def update_abstractmethods(cls):
|
||||
"""Recalculate the set of abstract methods of an abstract class.
|
||||
|
||||
If a class has had one of its abstract methods implemented after the
|
||||
class was created, the method will not be considered implemented until
|
||||
this function is called. Alternatively, if a new abstract method has been
|
||||
added to the class, it will only be considered an abstract method of the
|
||||
class after this function is called.
|
||||
|
||||
This function should be called before any use is made of the class,
|
||||
usually in class decorators that add methods to the subject class.
|
||||
|
||||
Returns cls, to allow usage as a class decorator.
|
||||
|
||||
If cls is not an instance of ABCMeta, does nothing.
|
||||
"""
|
||||
if not hasattr(cls, '__abstractmethods__'):
|
||||
# We check for __abstractmethods__ here because cls might by a C
|
||||
# implementation or a python implementation (especially during
|
||||
# testing), and we want to handle both cases.
|
||||
return cls
|
||||
|
||||
abstracts = set()
|
||||
# Check the existing abstract methods of the parents, keep only the ones
|
||||
# that are not implemented.
|
||||
for scls in cls.__bases__:
|
||||
for name in getattr(scls, '__abstractmethods__', ()):
|
||||
value = getattr(cls, name, None)
|
||||
if getattr(value, "__isabstractmethod__", False):
|
||||
abstracts.add(name)
|
||||
# Also add any other newly added abstract methods.
|
||||
for name, value in cls.__dict__.items():
|
||||
if getattr(value, "__isabstractmethod__", False):
|
||||
abstracts.add(name)
|
||||
cls.__abstractmethods__ = frozenset(abstracts)
|
||||
return cls
|
||||
|
||||
|
||||
class ABC(metaclass=ABCMeta):
|
||||
"""Helper class that provides a standard way to create an ABC using
|
||||
inheritance.
|
||||
|
||||
951
Lib/aifc.py
vendored
Normal file
951
Lib/aifc.py
vendored
Normal file
@@ -0,0 +1,951 @@
|
||||
"""Stuff to parse AIFF-C and AIFF files.
|
||||
|
||||
Unless explicitly stated otherwise, the description below is true
|
||||
both for AIFF-C files and AIFF files.
|
||||
|
||||
An AIFF-C file has the following structure.
|
||||
|
||||
+-----------------+
|
||||
| FORM |
|
||||
+-----------------+
|
||||
| <size> |
|
||||
+----+------------+
|
||||
| | AIFC |
|
||||
| +------------+
|
||||
| | <chunks> |
|
||||
| | . |
|
||||
| | . |
|
||||
| | . |
|
||||
+----+------------+
|
||||
|
||||
An AIFF file has the string "AIFF" instead of "AIFC".
|
||||
|
||||
A chunk consists of an identifier (4 bytes) followed by a size (4 bytes,
|
||||
big endian order), followed by the data. The size field does not include
|
||||
the size of the 8 byte header.
|
||||
|
||||
The following chunk types are recognized.
|
||||
|
||||
FVER
|
||||
<version number of AIFF-C defining document> (AIFF-C only).
|
||||
MARK
|
||||
<# of markers> (2 bytes)
|
||||
list of markers:
|
||||
<marker ID> (2 bytes, must be > 0)
|
||||
<position> (4 bytes)
|
||||
<marker name> ("pstring")
|
||||
COMM
|
||||
<# of channels> (2 bytes)
|
||||
<# of sound frames> (4 bytes)
|
||||
<size of the samples> (2 bytes)
|
||||
<sampling frequency> (10 bytes, IEEE 80-bit extended
|
||||
floating point)
|
||||
in AIFF-C files only:
|
||||
<compression type> (4 bytes)
|
||||
<human-readable version of compression type> ("pstring")
|
||||
SSND
|
||||
<offset> (4 bytes, not used by this program)
|
||||
<blocksize> (4 bytes, not used by this program)
|
||||
<sound data>
|
||||
|
||||
A pstring consists of 1 byte length, a string of characters, and 0 or 1
|
||||
byte pad to make the total length even.
|
||||
|
||||
Usage.
|
||||
|
||||
Reading AIFF files:
|
||||
f = aifc.open(file, 'r')
|
||||
where file is either the name of a file or an open file pointer.
|
||||
The open file pointer must have methods read(), seek(), and close().
|
||||
In some types of audio files, if the setpos() method is not used,
|
||||
the seek() method is not necessary.
|
||||
|
||||
This returns an instance of a class with the following public methods:
|
||||
getnchannels() -- returns number of audio channels (1 for
|
||||
mono, 2 for stereo)
|
||||
getsampwidth() -- returns sample width in bytes
|
||||
getframerate() -- returns sampling frequency
|
||||
getnframes() -- returns number of audio frames
|
||||
getcomptype() -- returns compression type ('NONE' for AIFF files)
|
||||
getcompname() -- returns human-readable version of
|
||||
compression type ('not compressed' for AIFF files)
|
||||
getparams() -- returns a namedtuple consisting of all of the
|
||||
above in the above order
|
||||
getmarkers() -- get the list of marks in the audio file or None
|
||||
if there are no marks
|
||||
getmark(id) -- get mark with the specified id (raises an error
|
||||
if the mark does not exist)
|
||||
readframes(n) -- returns at most n frames of audio
|
||||
rewind() -- rewind to the beginning of the audio stream
|
||||
setpos(pos) -- seek to the specified position
|
||||
tell() -- return the current position
|
||||
close() -- close the instance (make it unusable)
|
||||
The position returned by tell(), the position given to setpos() and
|
||||
the position of marks are all compatible and have nothing to do with
|
||||
the actual position in the file.
|
||||
The close() method is called automatically when the class instance
|
||||
is destroyed.
|
||||
|
||||
Writing AIFF files:
|
||||
f = aifc.open(file, 'w')
|
||||
where file is either the name of a file or an open file pointer.
|
||||
The open file pointer must have methods write(), tell(), seek(), and
|
||||
close().
|
||||
|
||||
This returns an instance of a class with the following public methods:
|
||||
aiff() -- create an AIFF file (AIFF-C default)
|
||||
aifc() -- create an AIFF-C file
|
||||
setnchannels(n) -- set the number of channels
|
||||
setsampwidth(n) -- set the sample width
|
||||
setframerate(n) -- set the frame rate
|
||||
setnframes(n) -- set the number of frames
|
||||
setcomptype(type, name)
|
||||
-- set the compression type and the
|
||||
human-readable compression type
|
||||
setparams(tuple)
|
||||
-- set all parameters at once
|
||||
setmark(id, pos, name)
|
||||
-- add specified mark to the list of marks
|
||||
tell() -- return current position in output file (useful
|
||||
in combination with setmark())
|
||||
writeframesraw(data)
|
||||
-- write audio frames without pathing up the
|
||||
file header
|
||||
writeframes(data)
|
||||
-- write audio frames and patch up the file header
|
||||
close() -- patch up the file header and close the
|
||||
output file
|
||||
You should set the parameters before the first writeframesraw or
|
||||
writeframes. The total number of frames does not need to be set,
|
||||
but when it is set to the correct value, the header does not have to
|
||||
be patched up.
|
||||
It is best to first set all parameters, perhaps possibly the
|
||||
compression type, and then write audio frames using writeframesraw.
|
||||
When all frames have been written, either call writeframes(b'') or
|
||||
close() to patch up the sizes in the header.
|
||||
Marks can be added anytime. If there are any marks, you must call
|
||||
close() after all frames have been written.
|
||||
The close() method is called automatically when the class instance
|
||||
is destroyed.
|
||||
|
||||
When a file is opened with the extension '.aiff', an AIFF file is
|
||||
written, otherwise an AIFF-C file is written. This default can be
|
||||
changed by calling aiff() or aifc() before the first writeframes or
|
||||
writeframesraw.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import builtins
|
||||
import warnings
|
||||
|
||||
__all__ = ["Error", "open", "openfp"]
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
_AIFC_version = 0xA2805140 # Version 1 of AIFF-C
|
||||
|
||||
def _read_long(file):
|
||||
try:
|
||||
return struct.unpack('>l', file.read(4))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_ulong(file):
|
||||
try:
|
||||
return struct.unpack('>L', file.read(4))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_short(file):
|
||||
try:
|
||||
return struct.unpack('>h', file.read(2))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_ushort(file):
|
||||
try:
|
||||
return struct.unpack('>H', file.read(2))[0]
|
||||
except struct.error:
|
||||
raise EOFError from None
|
||||
|
||||
def _read_string(file):
|
||||
length = ord(file.read(1))
|
||||
if length == 0:
|
||||
data = b''
|
||||
else:
|
||||
data = file.read(length)
|
||||
if length & 1 == 0:
|
||||
dummy = file.read(1)
|
||||
return data
|
||||
|
||||
_HUGE_VAL = 1.79769313486231e+308 # See <limits.h>
|
||||
|
||||
def _read_float(f): # 10 bytes
|
||||
expon = _read_short(f) # 2 bytes
|
||||
sign = 1
|
||||
if expon < 0:
|
||||
sign = -1
|
||||
expon = expon + 0x8000
|
||||
himant = _read_ulong(f) # 4 bytes
|
||||
lomant = _read_ulong(f) # 4 bytes
|
||||
if expon == himant == lomant == 0:
|
||||
f = 0.0
|
||||
elif expon == 0x7FFF:
|
||||
f = _HUGE_VAL
|
||||
else:
|
||||
expon = expon - 16383
|
||||
f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
|
||||
return sign * f
|
||||
|
||||
def _write_short(f, x):
|
||||
f.write(struct.pack('>h', x))
|
||||
|
||||
def _write_ushort(f, x):
|
||||
f.write(struct.pack('>H', x))
|
||||
|
||||
def _write_long(f, x):
|
||||
f.write(struct.pack('>l', x))
|
||||
|
||||
def _write_ulong(f, x):
|
||||
f.write(struct.pack('>L', x))
|
||||
|
||||
def _write_string(f, s):
|
||||
if len(s) > 255:
|
||||
raise ValueError("string exceeds maximum pstring length")
|
||||
f.write(struct.pack('B', len(s)))
|
||||
f.write(s)
|
||||
if len(s) & 1 == 0:
|
||||
f.write(b'\x00')
|
||||
|
||||
def _write_float(f, x):
|
||||
import math
|
||||
if x < 0:
|
||||
sign = 0x8000
|
||||
x = x * -1
|
||||
else:
|
||||
sign = 0
|
||||
if x == 0:
|
||||
expon = 0
|
||||
himant = 0
|
||||
lomant = 0
|
||||
else:
|
||||
fmant, expon = math.frexp(x)
|
||||
if expon > 16384 or fmant >= 1 or fmant != fmant: # Infinity or NaN
|
||||
expon = sign|0x7FFF
|
||||
himant = 0
|
||||
lomant = 0
|
||||
else: # Finite
|
||||
expon = expon + 16382
|
||||
if expon < 0: # denormalized
|
||||
fmant = math.ldexp(fmant, expon)
|
||||
expon = 0
|
||||
expon = expon | sign
|
||||
fmant = math.ldexp(fmant, 32)
|
||||
fsmant = math.floor(fmant)
|
||||
himant = int(fsmant)
|
||||
fmant = math.ldexp(fmant - fsmant, 32)
|
||||
fsmant = math.floor(fmant)
|
||||
lomant = int(fsmant)
|
||||
_write_ushort(f, expon)
|
||||
_write_ulong(f, himant)
|
||||
_write_ulong(f, lomant)
|
||||
|
||||
from chunk import Chunk
|
||||
from collections import namedtuple
|
||||
|
||||
_aifc_params = namedtuple('_aifc_params',
|
||||
'nchannels sampwidth framerate nframes comptype compname')
|
||||
|
||||
_aifc_params.nchannels.__doc__ = 'Number of audio channels (1 for mono, 2 for stereo)'
|
||||
_aifc_params.sampwidth.__doc__ = 'Sample width in bytes'
|
||||
_aifc_params.framerate.__doc__ = 'Sampling frequency'
|
||||
_aifc_params.nframes.__doc__ = 'Number of audio frames'
|
||||
_aifc_params.comptype.__doc__ = 'Compression type ("NONE" for AIFF files)'
|
||||
_aifc_params.compname.__doc__ = ("""\
|
||||
A human-readable version of the compression type
|
||||
('not compressed' for AIFF files)""")
|
||||
|
||||
|
||||
class Aifc_read:
|
||||
# Variables used in this class:
|
||||
#
|
||||
# These variables are available to the user though appropriate
|
||||
# methods of this class:
|
||||
# _file -- the open file with methods read(), close(), and seek()
|
||||
# set through the __init__() method
|
||||
# _nchannels -- the number of audio channels
|
||||
# available through the getnchannels() method
|
||||
# _nframes -- the number of audio frames
|
||||
# available through the getnframes() method
|
||||
# _sampwidth -- the number of bytes per audio sample
|
||||
# available through the getsampwidth() method
|
||||
# _framerate -- the sampling frequency
|
||||
# available through the getframerate() method
|
||||
# _comptype -- the AIFF-C compression type ('NONE' if AIFF)
|
||||
# available through the getcomptype() method
|
||||
# _compname -- the human-readable AIFF-C compression type
|
||||
# available through the getcomptype() method
|
||||
# _markers -- the marks in the audio file
|
||||
# available through the getmarkers() and getmark()
|
||||
# methods
|
||||
# _soundpos -- the position in the audio stream
|
||||
# available through the tell() method, set through the
|
||||
# setpos() method
|
||||
#
|
||||
# These variables are used internally only:
|
||||
# _version -- the AIFF-C version number
|
||||
# _decomp -- the decompressor from builtin module cl
|
||||
# _comm_chunk_read -- 1 iff the COMM chunk has been read
|
||||
# _aifc -- 1 iff reading an AIFF-C file
|
||||
# _ssnd_seek_needed -- 1 iff positioned correctly in audio
|
||||
# file for readframes()
|
||||
# _ssnd_chunk -- instantiation of a chunk class for the SSND chunk
|
||||
# _framesize -- size of one frame in the file
|
||||
|
||||
_file = None # Set here since __del__ checks it
|
||||
|
||||
def initfp(self, file):
|
||||
self._version = 0
|
||||
self._convert = None
|
||||
self._markers = []
|
||||
self._soundpos = 0
|
||||
self._file = file
|
||||
chunk = Chunk(file)
|
||||
if chunk.getname() != b'FORM':
|
||||
raise Error('file does not start with FORM id')
|
||||
formdata = chunk.read(4)
|
||||
if formdata == b'AIFF':
|
||||
self._aifc = 0
|
||||
elif formdata == b'AIFC':
|
||||
self._aifc = 1
|
||||
else:
|
||||
raise Error('not an AIFF or AIFF-C file')
|
||||
self._comm_chunk_read = 0
|
||||
self._ssnd_chunk = None
|
||||
while 1:
|
||||
self._ssnd_seek_needed = 1
|
||||
try:
|
||||
chunk = Chunk(self._file)
|
||||
except EOFError:
|
||||
break
|
||||
chunkname = chunk.getname()
|
||||
if chunkname == b'COMM':
|
||||
self._read_comm_chunk(chunk)
|
||||
self._comm_chunk_read = 1
|
||||
elif chunkname == b'SSND':
|
||||
self._ssnd_chunk = chunk
|
||||
dummy = chunk.read(8)
|
||||
self._ssnd_seek_needed = 0
|
||||
elif chunkname == b'FVER':
|
||||
self._version = _read_ulong(chunk)
|
||||
elif chunkname == b'MARK':
|
||||
self._readmark(chunk)
|
||||
chunk.skip()
|
||||
if not self._comm_chunk_read or not self._ssnd_chunk:
|
||||
raise Error('COMM chunk and/or SSND chunk missing')
|
||||
|
||||
def __init__(self, f):
|
||||
if isinstance(f, str):
|
||||
file_object = builtins.open(f, 'rb')
|
||||
try:
|
||||
self.initfp(file_object)
|
||||
except:
|
||||
file_object.close()
|
||||
raise
|
||||
else:
|
||||
# assume it is an open file object already
|
||||
self.initfp(f)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
#
|
||||
# User visible methods.
|
||||
#
|
||||
def getfp(self):
|
||||
return self._file
|
||||
|
||||
def rewind(self):
|
||||
self._ssnd_seek_needed = 1
|
||||
self._soundpos = 0
|
||||
|
||||
def close(self):
|
||||
file = self._file
|
||||
if file is not None:
|
||||
self._file = None
|
||||
file.close()
|
||||
|
||||
def tell(self):
|
||||
return self._soundpos
|
||||
|
||||
def getnchannels(self):
|
||||
return self._nchannels
|
||||
|
||||
def getnframes(self):
|
||||
return self._nframes
|
||||
|
||||
def getsampwidth(self):
|
||||
return self._sampwidth
|
||||
|
||||
def getframerate(self):
|
||||
return self._framerate
|
||||
|
||||
def getcomptype(self):
|
||||
return self._comptype
|
||||
|
||||
def getcompname(self):
|
||||
return self._compname
|
||||
|
||||
## def getversion(self):
|
||||
## return self._version
|
||||
|
||||
def getparams(self):
|
||||
return _aifc_params(self.getnchannels(), self.getsampwidth(),
|
||||
self.getframerate(), self.getnframes(),
|
||||
self.getcomptype(), self.getcompname())
|
||||
|
||||
def getmarkers(self):
|
||||
if len(self._markers) == 0:
|
||||
return None
|
||||
return self._markers
|
||||
|
||||
def getmark(self, id):
|
||||
for marker in self._markers:
|
||||
if id == marker[0]:
|
||||
return marker
|
||||
raise Error('marker {0!r} does not exist'.format(id))
|
||||
|
||||
def setpos(self, pos):
|
||||
if pos < 0 or pos > self._nframes:
|
||||
raise Error('position not in range')
|
||||
self._soundpos = pos
|
||||
self._ssnd_seek_needed = 1
|
||||
|
||||
def readframes(self, nframes):
|
||||
if self._ssnd_seek_needed:
|
||||
self._ssnd_chunk.seek(0)
|
||||
dummy = self._ssnd_chunk.read(8)
|
||||
pos = self._soundpos * self._framesize
|
||||
if pos:
|
||||
self._ssnd_chunk.seek(pos + 8)
|
||||
self._ssnd_seek_needed = 0
|
||||
if nframes == 0:
|
||||
return b''
|
||||
data = self._ssnd_chunk.read(nframes * self._framesize)
|
||||
if self._convert and data:
|
||||
data = self._convert(data)
|
||||
self._soundpos = self._soundpos + len(data) // (self._nchannels
|
||||
* self._sampwidth)
|
||||
return data
|
||||
|
||||
#
|
||||
# Internal methods.
|
||||
#
|
||||
|
||||
def _alaw2lin(self, data):
|
||||
import audioop
|
||||
return audioop.alaw2lin(data, 2)
|
||||
|
||||
def _ulaw2lin(self, data):
|
||||
import audioop
|
||||
return audioop.ulaw2lin(data, 2)
|
||||
|
||||
def _adpcm2lin(self, data):
|
||||
import audioop
|
||||
if not hasattr(self, '_adpcmstate'):
|
||||
# first time
|
||||
self._adpcmstate = None
|
||||
data, self._adpcmstate = audioop.adpcm2lin(data, 2, self._adpcmstate)
|
||||
return data
|
||||
|
||||
def _read_comm_chunk(self, chunk):
|
||||
self._nchannels = _read_short(chunk)
|
||||
self._nframes = _read_long(chunk)
|
||||
self._sampwidth = (_read_short(chunk) + 7) // 8
|
||||
self._framerate = int(_read_float(chunk))
|
||||
if self._sampwidth <= 0:
|
||||
raise Error('bad sample width')
|
||||
if self._nchannels <= 0:
|
||||
raise Error('bad # of channels')
|
||||
self._framesize = self._nchannels * self._sampwidth
|
||||
if self._aifc:
|
||||
#DEBUG: SGI's soundeditor produces a bad size :-(
|
||||
kludge = 0
|
||||
if chunk.chunksize == 18:
|
||||
kludge = 1
|
||||
warnings.warn('Warning: bad COMM chunk size')
|
||||
chunk.chunksize = 23
|
||||
#DEBUG end
|
||||
self._comptype = chunk.read(4)
|
||||
#DEBUG start
|
||||
if kludge:
|
||||
length = ord(chunk.file.read(1))
|
||||
if length & 1 == 0:
|
||||
length = length + 1
|
||||
chunk.chunksize = chunk.chunksize + length
|
||||
chunk.file.seek(-1, 1)
|
||||
#DEBUG end
|
||||
self._compname = _read_string(chunk)
|
||||
if self._comptype != b'NONE':
|
||||
if self._comptype == b'G722':
|
||||
self._convert = self._adpcm2lin
|
||||
elif self._comptype in (b'ulaw', b'ULAW'):
|
||||
self._convert = self._ulaw2lin
|
||||
elif self._comptype in (b'alaw', b'ALAW'):
|
||||
self._convert = self._alaw2lin
|
||||
else:
|
||||
raise Error('unsupported compression type')
|
||||
self._sampwidth = 2
|
||||
else:
|
||||
self._comptype = b'NONE'
|
||||
self._compname = b'not compressed'
|
||||
|
||||
def _readmark(self, chunk):
|
||||
nmarkers = _read_short(chunk)
|
||||
# Some files appear to contain invalid counts.
|
||||
# Cope with this by testing for EOF.
|
||||
try:
|
||||
for i in range(nmarkers):
|
||||
id = _read_short(chunk)
|
||||
pos = _read_long(chunk)
|
||||
name = _read_string(chunk)
|
||||
if pos or name:
|
||||
# some files appear to have
|
||||
# dummy markers consisting of
|
||||
# a position 0 and name ''
|
||||
self._markers.append((id, pos, name))
|
||||
except EOFError:
|
||||
w = ('Warning: MARK chunk contains only %s marker%s instead of %s' %
|
||||
(len(self._markers), '' if len(self._markers) == 1 else 's',
|
||||
nmarkers))
|
||||
warnings.warn(w)
|
||||
|
||||
class Aifc_write:
|
||||
# Variables used in this class:
|
||||
#
|
||||
# These variables are user settable through appropriate methods
|
||||
# of this class:
|
||||
# _file -- the open file with methods write(), close(), tell(), seek()
|
||||
# set through the __init__() method
|
||||
# _comptype -- the AIFF-C compression type ('NONE' in AIFF)
|
||||
# set through the setcomptype() or setparams() method
|
||||
# _compname -- the human-readable AIFF-C compression type
|
||||
# set through the setcomptype() or setparams() method
|
||||
# _nchannels -- the number of audio channels
|
||||
# set through the setnchannels() or setparams() method
|
||||
# _sampwidth -- the number of bytes per audio sample
|
||||
# set through the setsampwidth() or setparams() method
|
||||
# _framerate -- the sampling frequency
|
||||
# set through the setframerate() or setparams() method
|
||||
# _nframes -- the number of audio frames written to the header
|
||||
# set through the setnframes() or setparams() method
|
||||
# _aifc -- whether we're writing an AIFF-C file or an AIFF file
|
||||
# set through the aifc() method, reset through the
|
||||
# aiff() method
|
||||
#
|
||||
# These variables are used internally only:
|
||||
# _version -- the AIFF-C version number
|
||||
# _comp -- the compressor from builtin module cl
|
||||
# _nframeswritten -- the number of audio frames actually written
|
||||
# _datalength -- the size of the audio samples written to the header
|
||||
# _datawritten -- the size of the audio samples actually written
|
||||
|
||||
_file = None # Set here since __del__ checks it
|
||||
|
||||
def __init__(self, f):
|
||||
if isinstance(f, str):
|
||||
file_object = builtins.open(f, 'wb')
|
||||
try:
|
||||
self.initfp(file_object)
|
||||
except:
|
||||
file_object.close()
|
||||
raise
|
||||
|
||||
# treat .aiff file extensions as non-compressed audio
|
||||
if f.endswith('.aiff'):
|
||||
self._aifc = 0
|
||||
else:
|
||||
# assume it is an open file object already
|
||||
self.initfp(f)
|
||||
|
||||
def initfp(self, file):
|
||||
self._file = file
|
||||
self._version = _AIFC_version
|
||||
self._comptype = b'NONE'
|
||||
self._compname = b'not compressed'
|
||||
self._convert = None
|
||||
self._nchannels = 0
|
||||
self._sampwidth = 0
|
||||
self._framerate = 0
|
||||
self._nframes = 0
|
||||
self._nframeswritten = 0
|
||||
self._datawritten = 0
|
||||
self._datalength = 0
|
||||
self._markers = []
|
||||
self._marklength = 0
|
||||
self._aifc = 1 # AIFF-C is default
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
#
|
||||
# User visible methods.
|
||||
#
|
||||
def aiff(self):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
self._aifc = 0
|
||||
|
||||
def aifc(self):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
self._aifc = 1
|
||||
|
||||
def setnchannels(self, nchannels):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if nchannels < 1:
|
||||
raise Error('bad # of channels')
|
||||
self._nchannels = nchannels
|
||||
|
||||
def getnchannels(self):
|
||||
if not self._nchannels:
|
||||
raise Error('number of channels not set')
|
||||
return self._nchannels
|
||||
|
||||
def setsampwidth(self, sampwidth):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if sampwidth < 1 or sampwidth > 4:
|
||||
raise Error('bad sample width')
|
||||
self._sampwidth = sampwidth
|
||||
|
||||
def getsampwidth(self):
|
||||
if not self._sampwidth:
|
||||
raise Error('sample width not set')
|
||||
return self._sampwidth
|
||||
|
||||
def setframerate(self, framerate):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if framerate <= 0:
|
||||
raise Error('bad frame rate')
|
||||
self._framerate = framerate
|
||||
|
||||
def getframerate(self):
|
||||
if not self._framerate:
|
||||
raise Error('frame rate not set')
|
||||
return self._framerate
|
||||
|
||||
def setnframes(self, nframes):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
self._nframes = nframes
|
||||
|
||||
def getnframes(self):
|
||||
return self._nframeswritten
|
||||
|
||||
def setcomptype(self, comptype, compname):
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if comptype not in (b'NONE', b'ulaw', b'ULAW',
|
||||
b'alaw', b'ALAW', b'G722'):
|
||||
raise Error('unsupported compression type')
|
||||
self._comptype = comptype
|
||||
self._compname = compname
|
||||
|
||||
def getcomptype(self):
|
||||
return self._comptype
|
||||
|
||||
def getcompname(self):
|
||||
return self._compname
|
||||
|
||||
## def setversion(self, version):
|
||||
## if self._nframeswritten:
|
||||
## raise Error, 'cannot change parameters after starting to write'
|
||||
## self._version = version
|
||||
|
||||
def setparams(self, params):
|
||||
nchannels, sampwidth, framerate, nframes, comptype, compname = params
|
||||
if self._nframeswritten:
|
||||
raise Error('cannot change parameters after starting to write')
|
||||
if comptype not in (b'NONE', b'ulaw', b'ULAW',
|
||||
b'alaw', b'ALAW', b'G722'):
|
||||
raise Error('unsupported compression type')
|
||||
self.setnchannels(nchannels)
|
||||
self.setsampwidth(sampwidth)
|
||||
self.setframerate(framerate)
|
||||
self.setnframes(nframes)
|
||||
self.setcomptype(comptype, compname)
|
||||
|
||||
def getparams(self):
|
||||
if not self._nchannels or not self._sampwidth or not self._framerate:
|
||||
raise Error('not all parameters set')
|
||||
return _aifc_params(self._nchannels, self._sampwidth, self._framerate,
|
||||
self._nframes, self._comptype, self._compname)
|
||||
|
||||
def setmark(self, id, pos, name):
|
||||
if id <= 0:
|
||||
raise Error('marker ID must be > 0')
|
||||
if pos < 0:
|
||||
raise Error('marker position must be >= 0')
|
||||
if not isinstance(name, bytes):
|
||||
raise Error('marker name must be bytes')
|
||||
for i in range(len(self._markers)):
|
||||
if id == self._markers[i][0]:
|
||||
self._markers[i] = id, pos, name
|
||||
return
|
||||
self._markers.append((id, pos, name))
|
||||
|
||||
def getmark(self, id):
|
||||
for marker in self._markers:
|
||||
if id == marker[0]:
|
||||
return marker
|
||||
raise Error('marker {0!r} does not exist'.format(id))
|
||||
|
||||
def getmarkers(self):
|
||||
if len(self._markers) == 0:
|
||||
return None
|
||||
return self._markers
|
||||
|
||||
def tell(self):
|
||||
return self._nframeswritten
|
||||
|
||||
def writeframesraw(self, data):
|
||||
if not isinstance(data, (bytes, bytearray)):
|
||||
data = memoryview(data).cast('B')
|
||||
self._ensure_header_written(len(data))
|
||||
nframes = len(data) // (self._sampwidth * self._nchannels)
|
||||
if self._convert:
|
||||
data = self._convert(data)
|
||||
self._file.write(data)
|
||||
self._nframeswritten = self._nframeswritten + nframes
|
||||
self._datawritten = self._datawritten + len(data)
|
||||
|
||||
def writeframes(self, data):
|
||||
self.writeframesraw(data)
|
||||
if self._nframeswritten != self._nframes or \
|
||||
self._datalength != self._datawritten:
|
||||
self._patchheader()
|
||||
|
||||
def close(self):
|
||||
if self._file is None:
|
||||
return
|
||||
try:
|
||||
self._ensure_header_written(0)
|
||||
if self._datawritten & 1:
|
||||
# quick pad to even size
|
||||
self._file.write(b'\x00')
|
||||
self._datawritten = self._datawritten + 1
|
||||
self._writemarkers()
|
||||
if self._nframeswritten != self._nframes or \
|
||||
self._datalength != self._datawritten or \
|
||||
self._marklength:
|
||||
self._patchheader()
|
||||
finally:
|
||||
# Prevent ref cycles
|
||||
self._convert = None
|
||||
f = self._file
|
||||
self._file = None
|
||||
f.close()
|
||||
|
||||
#
|
||||
# Internal methods.
|
||||
#
|
||||
|
||||
def _lin2alaw(self, data):
|
||||
import audioop
|
||||
return audioop.lin2alaw(data, 2)
|
||||
|
||||
def _lin2ulaw(self, data):
|
||||
import audioop
|
||||
return audioop.lin2ulaw(data, 2)
|
||||
|
||||
def _lin2adpcm(self, data):
|
||||
import audioop
|
||||
if not hasattr(self, '_adpcmstate'):
|
||||
self._adpcmstate = None
|
||||
data, self._adpcmstate = audioop.lin2adpcm(data, 2, self._adpcmstate)
|
||||
return data
|
||||
|
||||
def _ensure_header_written(self, datasize):
|
||||
if not self._nframeswritten:
|
||||
if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
|
||||
if not self._sampwidth:
|
||||
self._sampwidth = 2
|
||||
if self._sampwidth != 2:
|
||||
raise Error('sample width must be 2 when compressing '
|
||||
'with ulaw/ULAW, alaw/ALAW or G7.22 (ADPCM)')
|
||||
if not self._nchannels:
|
||||
raise Error('# channels not specified')
|
||||
if not self._sampwidth:
|
||||
raise Error('sample width not specified')
|
||||
if not self._framerate:
|
||||
raise Error('sampling rate not specified')
|
||||
self._write_header(datasize)
|
||||
|
||||
def _init_compression(self):
|
||||
if self._comptype == b'G722':
|
||||
self._convert = self._lin2adpcm
|
||||
elif self._comptype in (b'ulaw', b'ULAW'):
|
||||
self._convert = self._lin2ulaw
|
||||
elif self._comptype in (b'alaw', b'ALAW'):
|
||||
self._convert = self._lin2alaw
|
||||
|
||||
def _write_header(self, initlength):
|
||||
if self._aifc and self._comptype != b'NONE':
|
||||
self._init_compression()
|
||||
self._file.write(b'FORM')
|
||||
if not self._nframes:
|
||||
self._nframes = initlength // (self._nchannels * self._sampwidth)
|
||||
self._datalength = self._nframes * self._nchannels * self._sampwidth
|
||||
if self._datalength & 1:
|
||||
self._datalength = self._datalength + 1
|
||||
if self._aifc:
|
||||
if self._comptype in (b'ulaw', b'ULAW', b'alaw', b'ALAW'):
|
||||
self._datalength = self._datalength // 2
|
||||
if self._datalength & 1:
|
||||
self._datalength = self._datalength + 1
|
||||
elif self._comptype == b'G722':
|
||||
self._datalength = (self._datalength + 3) // 4
|
||||
if self._datalength & 1:
|
||||
self._datalength = self._datalength + 1
|
||||
try:
|
||||
self._form_length_pos = self._file.tell()
|
||||
except (AttributeError, OSError):
|
||||
self._form_length_pos = None
|
||||
commlength = self._write_form_length(self._datalength)
|
||||
if self._aifc:
|
||||
self._file.write(b'AIFC')
|
||||
self._file.write(b'FVER')
|
||||
_write_ulong(self._file, 4)
|
||||
_write_ulong(self._file, self._version)
|
||||
else:
|
||||
self._file.write(b'AIFF')
|
||||
self._file.write(b'COMM')
|
||||
_write_ulong(self._file, commlength)
|
||||
_write_short(self._file, self._nchannels)
|
||||
if self._form_length_pos is not None:
|
||||
self._nframes_pos = self._file.tell()
|
||||
_write_ulong(self._file, self._nframes)
|
||||
if self._comptype in (b'ULAW', b'ulaw', b'ALAW', b'alaw', b'G722'):
|
||||
_write_short(self._file, 8)
|
||||
else:
|
||||
_write_short(self._file, self._sampwidth * 8)
|
||||
_write_float(self._file, self._framerate)
|
||||
if self._aifc:
|
||||
self._file.write(self._comptype)
|
||||
_write_string(self._file, self._compname)
|
||||
self._file.write(b'SSND')
|
||||
if self._form_length_pos is not None:
|
||||
self._ssnd_length_pos = self._file.tell()
|
||||
_write_ulong(self._file, self._datalength + 8)
|
||||
_write_ulong(self._file, 0)
|
||||
_write_ulong(self._file, 0)
|
||||
|
||||
def _write_form_length(self, datalength):
|
||||
if self._aifc:
|
||||
commlength = 18 + 5 + len(self._compname)
|
||||
if commlength & 1:
|
||||
commlength = commlength + 1
|
||||
verslength = 12
|
||||
else:
|
||||
commlength = 18
|
||||
verslength = 0
|
||||
_write_ulong(self._file, 4 + verslength + self._marklength + \
|
||||
8 + commlength + 16 + datalength)
|
||||
return commlength
|
||||
|
||||
def _patchheader(self):
|
||||
curpos = self._file.tell()
|
||||
if self._datawritten & 1:
|
||||
datalength = self._datawritten + 1
|
||||
self._file.write(b'\x00')
|
||||
else:
|
||||
datalength = self._datawritten
|
||||
if datalength == self._datalength and \
|
||||
self._nframes == self._nframeswritten and \
|
||||
self._marklength == 0:
|
||||
self._file.seek(curpos, 0)
|
||||
return
|
||||
self._file.seek(self._form_length_pos, 0)
|
||||
dummy = self._write_form_length(datalength)
|
||||
self._file.seek(self._nframes_pos, 0)
|
||||
_write_ulong(self._file, self._nframeswritten)
|
||||
self._file.seek(self._ssnd_length_pos, 0)
|
||||
_write_ulong(self._file, datalength + 8)
|
||||
self._file.seek(curpos, 0)
|
||||
self._nframes = self._nframeswritten
|
||||
self._datalength = datalength
|
||||
|
||||
def _writemarkers(self):
|
||||
if len(self._markers) == 0:
|
||||
return
|
||||
self._file.write(b'MARK')
|
||||
length = 2
|
||||
for marker in self._markers:
|
||||
id, pos, name = marker
|
||||
length = length + len(name) + 1 + 6
|
||||
if len(name) & 1 == 0:
|
||||
length = length + 1
|
||||
_write_ulong(self._file, length)
|
||||
self._marklength = length + 8
|
||||
_write_short(self._file, len(self._markers))
|
||||
for marker in self._markers:
|
||||
id, pos, name = marker
|
||||
_write_short(self._file, id)
|
||||
_write_ulong(self._file, pos)
|
||||
_write_string(self._file, name)
|
||||
|
||||
def open(f, mode=None):
|
||||
if mode is None:
|
||||
if hasattr(f, 'mode'):
|
||||
mode = f.mode
|
||||
else:
|
||||
mode = 'rb'
|
||||
if mode in ('r', 'rb'):
|
||||
return Aifc_read(f)
|
||||
elif mode in ('w', 'wb'):
|
||||
return Aifc_write(f)
|
||||
else:
|
||||
raise Error("mode must be 'r', 'rb', 'w', or 'wb'")
|
||||
|
||||
def openfp(f, mode=None):
|
||||
warnings.warn("aifc.openfp is deprecated since Python 3.7. "
|
||||
"Use aifc.open instead.", DeprecationWarning, stacklevel=2)
|
||||
return open(f, mode=mode)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
if not sys.argv[1:]:
|
||||
sys.argv.append('/usr/demos/data/audio/bach.aiff')
|
||||
fn = sys.argv[1]
|
||||
with open(fn, 'r') as f:
|
||||
print("Reading", fn)
|
||||
print("nchannels =", f.getnchannels())
|
||||
print("nframes =", f.getnframes())
|
||||
print("sampwidth =", f.getsampwidth())
|
||||
print("framerate =", f.getframerate())
|
||||
print("comptype =", f.getcomptype())
|
||||
print("compname =", f.getcompname())
|
||||
if sys.argv[2:]:
|
||||
gn = sys.argv[2]
|
||||
print("Writing", gn)
|
||||
with open(gn, 'w') as g:
|
||||
g.setparams(f.getparams())
|
||||
while 1:
|
||||
data = f.readframes(1024)
|
||||
if not data:
|
||||
break
|
||||
g.writeframes(data)
|
||||
print("Done.")
|
||||
4
Lib/antigravity.py
vendored
4
Lib/antigravity.py
vendored
@@ -11,7 +11,7 @@ def geohash(latitude, longitude, datedow):
|
||||
37.857713 -122.544543
|
||||
|
||||
'''
|
||||
# https://xkcd.com/426/
|
||||
h = hashlib.md5(datedow, usedforsecurity=False).hexdigest()
|
||||
# http://xkcd.com/426/
|
||||
h = hashlib.md5(datedow).hexdigest()
|
||||
p, q = [('%f' % float.fromhex('0.' + x)) for x in (h[:16], h[16:32])]
|
||||
print('%d%s %d%s' % (latitude, p[1:], longitude, q[1:]))
|
||||
|
||||
839
Lib/argparse.py
vendored
839
Lib/argparse.py
vendored
File diff suppressed because it is too large
Load Diff
1680
Lib/ast.py
vendored
1680
Lib/ast.py
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,22 @@
|
||||
"""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 *
|
||||
@@ -17,15 +25,11 @@ 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__ +
|
||||
@@ -34,9 +38,6 @@ __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
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
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
@@ -1,8 +1,18 @@
|
||||
__all__ = ()
|
||||
__all__ = []
|
||||
|
||||
import concurrent.futures._base
|
||||
import reprlib
|
||||
|
||||
from . import format_helpers
|
||||
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."""
|
||||
|
||||
|
||||
# States for Future.
|
||||
_PENDING = 'PENDING'
|
||||
@@ -28,17 +38,17 @@ def _format_callbacks(cb):
|
||||
cb = ''
|
||||
|
||||
def format_cb(callback):
|
||||
return format_helpers._format_callback_source(callback, ())
|
||||
return events._format_callback_source(callback, ())
|
||||
|
||||
if size == 1:
|
||||
cb = format_cb(cb[0][0])
|
||||
cb = format_cb(cb[0])
|
||||
elif size == 2:
|
||||
cb = '{}, {}'.format(format_cb(cb[0][0]), format_cb(cb[1][0]))
|
||||
cb = '{}, {}'.format(format_cb(cb[0]), format_cb(cb[1]))
|
||||
elif size > 2:
|
||||
cb = '{}, <{} more>, {}'.format(format_cb(cb[0][0]),
|
||||
cb = '{}, <{} more>, {}'.format(format_cb(cb[0]),
|
||||
size - 2,
|
||||
format_cb(cb[-1][0]))
|
||||
return f'cb=[{cb}]'
|
||||
format_cb(cb[-1]))
|
||||
return 'cb=[%s]' % cb
|
||||
|
||||
|
||||
def _future_repr_info(future):
|
||||
@@ -47,21 +57,15 @@ def _future_repr_info(future):
|
||||
info = [future._state.lower()]
|
||||
if future._state == _FINISHED:
|
||||
if future._exception is not None:
|
||||
info.append(f'exception={future._exception!r}')
|
||||
info.append('exception={!r}'.format(future._exception))
|
||||
else:
|
||||
# use reprlib to limit the length of the output, especially
|
||||
# for very long strings
|
||||
result = reprlib.repr(future._result)
|
||||
info.append(f'result={result}')
|
||||
info.append('result={}'.format(result))
|
||||
if future._callbacks:
|
||||
info.append(_format_callbacks(future._callbacks))
|
||||
if future._source_traceback:
|
||||
frame = future._source_traceback[-1]
|
||||
info.append(f'created at {frame[0]}:{frame[1]}')
|
||||
info.append('created at %s:%s' % (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}>'
|
||||
|
||||
@@ -2,8 +2,10 @@ import collections
|
||||
import subprocess
|
||||
import warnings
|
||||
|
||||
from . import compat
|
||||
from . import protocols
|
||||
from . import transports
|
||||
from .coroutines import coroutine
|
||||
from .log import logger
|
||||
|
||||
|
||||
@@ -57,9 +59,9 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
if self._closed:
|
||||
info.append('closed')
|
||||
if self._pid is not None:
|
||||
info.append(f'pid={self._pid}')
|
||||
info.append('pid=%s' % self._pid)
|
||||
if self._returncode is not None:
|
||||
info.append(f'returncode={self._returncode}')
|
||||
info.append('returncode=%s' % self._returncode)
|
||||
elif self._pid is not None:
|
||||
info.append('running')
|
||||
else:
|
||||
@@ -67,19 +69,19 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
|
||||
stdin = self._pipes.get(0)
|
||||
if stdin is not None:
|
||||
info.append(f'stdin={stdin.pipe}')
|
||||
info.append('stdin=%s' % stdin.pipe)
|
||||
|
||||
stdout = self._pipes.get(1)
|
||||
stderr = self._pipes.get(2)
|
||||
if stdout is not None and stderr is stdout:
|
||||
info.append(f'stdout=stderr={stdout.pipe}')
|
||||
info.append('stdout=stderr=%s' % stdout.pipe)
|
||||
else:
|
||||
if stdout is not None:
|
||||
info.append(f'stdout={stdout.pipe}')
|
||||
info.append('stdout=%s' % stdout.pipe)
|
||||
if stderr is not None:
|
||||
info.append(f'stderr={stderr.pipe}')
|
||||
info.append('stderr=%s' % stderr.pipe)
|
||||
|
||||
return '<{}>'.format(' '.join(info))
|
||||
return '<%s>' % ' '.join(info)
|
||||
|
||||
def _start(self, args, shell, stdin, stdout, stderr, bufsize, **kwargs):
|
||||
raise NotImplementedError
|
||||
@@ -103,13 +105,12 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
continue
|
||||
proto.pipe.close()
|
||||
|
||||
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._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._loop.get_debug():
|
||||
logger.warning('Close running child process: kill %r', self)
|
||||
|
||||
@@ -120,10 +121,15 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
|
||||
# Don't clear the _proc reference yet: _post_init() may still run
|
||||
|
||||
def __del__(self, _warn=warnings.warn):
|
||||
if not self._closed:
|
||||
_warn(f"unclosed transport {self!r}", ResourceWarning, source=self)
|
||||
self.close()
|
||||
# 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 get_pid(self):
|
||||
return self._pid
|
||||
@@ -153,25 +159,26 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
self._check_proc()
|
||||
self._proc.kill()
|
||||
|
||||
async def _connect_pipes(self, waiter):
|
||||
@coroutine
|
||||
def _connect_pipes(self, waiter):
|
||||
try:
|
||||
proc = self._proc
|
||||
loop = self._loop
|
||||
|
||||
if proc.stdin is not None:
|
||||
_, pipe = await loop.connect_write_pipe(
|
||||
_, pipe = yield from loop.connect_write_pipe(
|
||||
lambda: WriteSubprocessPipeProto(self, 0),
|
||||
proc.stdin)
|
||||
self._pipes[0] = pipe
|
||||
|
||||
if proc.stdout is not None:
|
||||
_, pipe = await loop.connect_read_pipe(
|
||||
_, pipe = yield from loop.connect_read_pipe(
|
||||
lambda: ReadSubprocessPipeProto(self, 1),
|
||||
proc.stdout)
|
||||
self._pipes[1] = pipe
|
||||
|
||||
if proc.stderr is not None:
|
||||
_, pipe = await loop.connect_read_pipe(
|
||||
_, pipe = yield from loop.connect_read_pipe(
|
||||
lambda: ReadSubprocessPipeProto(self, 2),
|
||||
proc.stderr)
|
||||
self._pipes[2] = pipe
|
||||
@@ -182,9 +189,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
for callback, data in self._pending_calls:
|
||||
loop.call_soon(callback, *data)
|
||||
self._pending_calls = None
|
||||
except (SystemExit, KeyboardInterrupt):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
except Exception as exc:
|
||||
if waiter is not None and not waiter.cancelled():
|
||||
waiter.set_exception(exc)
|
||||
else:
|
||||
@@ -208,17 +213,24 @@ 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()
|
||||
|
||||
async def _wait(self):
|
||||
# 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):
|
||||
"""Wait until the process exit and return the process return code.
|
||||
|
||||
This method is a coroutine."""
|
||||
@@ -227,7 +239,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
|
||||
|
||||
waiter = self._loop.create_future()
|
||||
self._exit_waiters.append(waiter)
|
||||
return await waiter
|
||||
return (yield from waiter)
|
||||
|
||||
def _try_finish(self):
|
||||
assert not self._finished
|
||||
@@ -242,11 +254,6 @@ 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
|
||||
@@ -264,7 +271,8 @@ class WriteSubprocessPipeProto(protocols.BaseProtocol):
|
||||
self.pipe = transport
|
||||
|
||||
def __repr__(self):
|
||||
return f'<{self.__class__.__name__} fd={self.fd} pipe={self.pipe!r}>'
|
||||
return ('<%s fd=%s pipe=%r>'
|
||||
% (self.__class__.__name__, self.fd, self.pipe))
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.disconnected = True
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import linecache
|
||||
import reprlib
|
||||
import traceback
|
||||
|
||||
from . import base_futures
|
||||
@@ -9,42 +8,25 @@ from . import coroutines
|
||||
def _task_repr_info(task):
|
||||
info = base_futures._future_repr_info(task)
|
||||
|
||||
if task.cancelling() and not task.done():
|
||||
if task._must_cancel:
|
||||
# replace status
|
||||
info[0] = 'cancelling'
|
||||
|
||||
info.insert(1, 'name=%r' % task.get_name())
|
||||
coro = coroutines._format_coroutine(task._coro)
|
||||
info.insert(1, 'coro=<%s>' % coro)
|
||||
|
||||
if task._fut_waiter is not None:
|
||||
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}>')
|
||||
|
||||
info.insert(2, 'wait_for=%r' % task._fut_waiter)
|
||||
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 = []
|
||||
if hasattr(task._coro, 'cr_frame'):
|
||||
# case 1: 'async def' coroutines
|
||||
try:
|
||||
# 'async def' coroutines
|
||||
f = task._coro.cr_frame
|
||||
elif hasattr(task._coro, 'gi_frame'):
|
||||
# case 2: legacy coroutines
|
||||
except AttributeError:
|
||||
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:
|
||||
@@ -79,15 +61,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(f'No stack for {task!r}', file=file)
|
||||
print('No stack for %r' % task, file=file)
|
||||
elif exc is not None:
|
||||
print(f'Traceback for {task!r} (most recent call last):', file=file)
|
||||
print('Traceback for %r (most recent call last):' % task,
|
||||
file=file)
|
||||
else:
|
||||
print(f'Stack for {task!r} (most recent call last):', file=file)
|
||||
|
||||
print('Stack for %r (most recent call last):' % task,
|
||||
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
Normal file
18
Lib/asyncio/compat.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""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)
|
||||
@@ -1,41 +1,7 @@
|
||||
# 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
|
||||
"""Constants."""
|
||||
|
||||
# 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()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user