Compare commits

..

1 Commits

Author SHA1 Message Date
Jeong, YunWon
39a6486a4c Release v0.5.0 2026-03-31 21:06:00 +09:00
824 changed files with 35415 additions and 130945 deletions

View File

@@ -4,8 +4,6 @@ argdefs
argtypes
asdl
asname
atopen
atext
attro
augassign
badcert
@@ -32,12 +30,9 @@ cellvar
cellvars
ceval
cfield
cfws
CFWS
CLASSDEREF
classdict
cmpop
CNOTAB
codedepth
CODEUNIT
CONIN
@@ -52,16 +47,13 @@ datastack
defaultdict
denom
deopt
deopts
dictbytype
DICTFLAG
dictoffset
distpoint
dynload
elts
eooh
eofs
EOOH
evalloop
excepthandler
exceptiontable
@@ -70,7 +62,6 @@ fastlocals
fblock
fblocks
fdescr
fdst
ffi_argtypes
fielddesc
fieldlist
@@ -84,7 +75,6 @@ freelist
freevar
freevars
fromlist
fsrc
getdict
getfunc
getiter
@@ -99,39 +89,28 @@ HASUNION
heaptype
hexdigit
HIGHRES
ialloc
IFUNC
IMMUTABLETYPE
INCREF
inlinedepth
inplace
inpos
ioffset
isbytecode
ishidden
ismine
ISPOINTER
isoctal
iteminfo
Itertool
iused
keeped
kwnames
kwonlyarg
kwonlyargs
kwonlydefaults
lasti
libffi
linearise
lineful
lineiterator
linetable
LNOTAB
loadfast
localsplus
localspluskinds
Lshift
lslpp
lsprof
MAXBLOCKS
maxdepth
@@ -141,23 +120,16 @@ mult
multibytecodec
nameobj
nameop
nargsf
nblocks
ncells
ncellsused
ncellvars
nconsts
newargs
newfree
NEWLOCALS
newsemlockobject
nextop
nfrees
nkwargs
nkwelts
nlocalsplus
nointerrupt
noffsets
Nondescriptor
noninteger
nops
@@ -165,7 +137,6 @@ noraise
nseen
NSIGNALS
numer
nvars
opname
opnames
orelse
@@ -178,22 +149,18 @@ patma
peepholer
phcount
platstdlib
ploc
posonlyarg
posonlyargs
prec
preds
preinitialized
pybuilddir
pycore
pyinner
pydecimal
pyerrors
Pyfunc
pylifecycle
pymain
pyrepl
pystate
PYTHONTRACEMALLOC
PYTHONUTF8
pythonw
@@ -201,7 +168,6 @@ PYTHREAD_NAME
releasebuffer
repr
resinfo
retarget
Rshift
SA_ONSTACK
saveall
@@ -213,7 +179,6 @@ SETREF
setresult
setslice
settraceallthreads
sget
SLOTDEFINED
SMALLBUF
SOABI
@@ -224,16 +189,13 @@ staticbase
stginfo
storefast
stringlib
stringized
structseq
subkwargs
subparams
subscr
sval
swappedbytes
swaptimize
sysdict
tbstderr
templatelib
testconsole
threadstate
@@ -252,7 +214,6 @@ uncollectable
Unhandle
unparse
unparser
untargeted
untracking
VARKEYWORDS
varkwarg

View File

@@ -67,7 +67,6 @@ fillchar
fillvalue
finallyhandler
firstiter
fobj
firstlineno
fnctl
frombytes
@@ -112,14 +111,12 @@ idfunc
idiv
idxs
impls
infd
indexgroup
infj
inittab
Inittab
instancecheck
instanceof
instrs
interpchannels
interpqueues
irepeat
@@ -178,7 +175,6 @@ Nonprintable
onceregistry
origname
ospath
outfd
pendingcr
phello
platlibdir
@@ -189,12 +185,10 @@ posonlyargcount
prepending
profilefunc
pycache
pycapsule
pycodecs
pycs
pydatetime
pyexpat
PYGILSTATE
pyio
pymain
PYTHONAPI

View File

@@ -49,25 +49,20 @@
"ignorePaths": [
"**/__pycache__/**",
"target/**",
"Lib/**",
"crates/host_env/**"
"Lib/**"
],
// words - list of words to be always considered correct
// (compound words like pyarg, baseclass, microbenchmark are handled by allowCompoundWords)
"words": [
"aiterable",
"alnum",
"csock",
"coro",
"dedentations",
"dedents",
"deduped",
"deoptimized",
"deoptimize",
"emscripten",
"excs",
"fnfe",
"ifexp",
"interps",
"jitted",
"jitting",
@@ -77,13 +72,8 @@
"oparg",
"opargs",
"pyc",
"reborrow",
"reraises",
"reraising",
"significand",
"summands",
"TESTFN",
"TZPATH",
"unraisable",
"wasi",
"weaked",

19
.gitattributes vendored
View File

@@ -58,14 +58,13 @@ Lib/venv/scripts/posix/* text eol=lf
#
[attr]generated linguist-generated=true diff=generated
Lib/_opcode_metadata.py generated
Lib/keyword.py generated
Lib/idlelib/help.html generated
Lib/test/certdata/*.pem generated
Lib/test/certdata/*.0 generated
Lib/test/levenshtein_examples.json generated
Lib/test/test_stable_abi_ctypes.py generated
Lib/token.py generated
crates/compiler-core/src/bytecode/opcode_metadata.rs generated
Lib/_opcode_metadata.py generated
Lib/keyword.py generated
Lib/idlelib/help.html generated
Lib/test/certdata/*.pem generated
Lib/test/certdata/*.0 generated
Lib/test/levenshtein_examples.json generated
Lib/test/test_stable_abi_ctypes.py generated
Lib/token.py generated
.github/workflows/*.lock.yml linguist-generated=true merge=ours
.github/workflows/*.lock.yml linguist-generated=true merge=ours

View File

@@ -1,10 +0,0 @@
<!--
Thanks for your contribution!
-->
- [ ] Closes #xxxx <!-- Replace xxxx with the GitHub issue number -->
- [ ] This PR follows our [AI policy](https://github.com/RustPython/.github/blob/main/AI_POLICY.md)
## Summary
<!-- What's the purpose of the change? What does it do, and why? -->

View File

@@ -2,9 +2,7 @@
version: 2
updates:
- package-ecosystem: cargo
directories:
- "/"
- "crates/*"
directory: /
schedule:
interval: weekly
cooldown:
@@ -21,7 +19,6 @@ updates:
- "digest"
- "md-5"
- "sha-1"
- "sha1"
- "sha2"
- "sha3"
- "blake2"
@@ -123,11 +120,6 @@ updates:
toml:
patterns:
- "toml*"
unix:
patterns:
- "mac_address"
- "nix"
- "rustyline"
wasm-bindgen:
patterns:
- "wasm-bindgen*"

View File

@@ -19,63 +19,21 @@ concurrency:
cancel-in-progress: true
env:
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls,host_env
CARGO_ARGS_NO_SSL: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,host_env
# Crates excluded from workspace builds:
# - rustpython_wasm: requires wasm target
# - rustpython-compiler-source: deprecated
# - rustpython-venvlauncher: Windows-only
WORKSPACE_EXCLUDES: --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher
# Python version targeted by the CI.
PYTHON_VERSION: "3.14.3"
X86_64_PC_WINDOWS_MSVC_OPENSSL_LIB_DIR: C:\Program Files\OpenSSL\lib\VC\x64\MD
X86_64_PC_WINDOWS_MSVC_OPENSSL_INCLUDE_DIR: C:\Program Files\OpenSSL\include
CARGO_INCREMENTAL: 0
CARGO_PROFILE_TEST_DEBUG: 0
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_RELEASE_DEBUG: 0
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # TODO: Remove on 2026/06/02
CI: true
jobs:
determine_changes:
name: Determine changes
runs-on: ubuntu-slim
outputs:
# Flag that is raised when any rust code is changed.
rust_code: ${{ steps.check_rust_code.outputs.changed }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- name: Determine merge base
id: merge_base
run: |
sha=$(git merge-base HEAD "origin/${BASE_REF}")
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
env:
BASE_REF: ${{ github.event.pull_request.base.ref || 'main' }}
- name: Check if there was any code related change
id: check_rust_code
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
':Cargo.toml' \
':Cargo.lock' \
':rust-toolchain.toml' \
':.cargo/config.toml' \
':crates/**' \
':src/**' \
':.github/workflows/ci.yaml' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
rust_tests:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
env:
@@ -92,36 +50,29 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-
restore-keys: |
${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable--
# Windows runners randomly crashes, https://github.com/actions/cache/issues/1754
continue-on-error: true
components: clippy
toolchain: stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install macOS dependencies
uses: ./.github/actions/install-macos-deps
- name: run rust tests
run: cargo test --workspace --exclude rustpython-capi ${{ env.WORKSPACE_EXCLUDES }} --features threading ${{ env.CARGO_ARGS }}
env:
INSTA_WORKSPACE_ROOT: ${{ github.workspace }}
- name: run clippy
run: cargo clippy ${{ env.CARGO_ARGS }} --workspace --all-targets ${{ env.WORKSPACE_EXCLUDES }} -- -Dwarnings
- name: run c-api tests
working-directory: crates/capi
run: cargo test
if: runner.os != 'Windows' # Requires pyo3 0.29+ on Windows
- name: run rust tests
run: cargo test --workspace ${{ env.WORKSPACE_EXCLUDES }} --verbose --features threading ${{ env.CARGO_ARGS }}
- name: check compilation without threading
run: cargo check ${{ env.CARGO_ARGS }}
- run: cargo doc --locked
if: runner.os == 'Linux'
- name: check compilation without host_env (sandbox mode)
run: |
@@ -140,10 +91,6 @@ jobs:
run: cargo build --no-default-features --features ssl-openssl
if: runner.os == 'Linux'
- name: Test vendored OpenSSL build
run: cargo build --no-default-features --features ssl-openssl-vendor
if: runner.os == 'Linux'
# - name: Install tk-dev for tkinter build
# run: sudo apt-get update && sudo apt-get install -y tk-dev
# if: runner.os == 'Linux'
@@ -165,18 +112,12 @@ jobs:
if: runner.os == 'Linux'
cargo_check:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
name: cargo check
runs-on: ${{ matrix.os }}
needs:
- determine_changes
if: |
(
!contains(github.event.pull_request.labels.*.name, 'skip:ci') &&
needs.determine_changes.outputs.rust_code == 'true'
) || github.ref == 'refs/heads/main'
strategy:
matrix:
include:
include:
- os: ubuntu-latest
target: aarch64-linux-android
- os: ubuntu-latest
@@ -187,13 +128,10 @@ jobs:
target: i686-unknown-linux-musl
dependencies:
musl-tools: true
skip_ssl: true
- os: ubuntu-latest
target: wasm32-wasip2
skip_ssl: true
- os: ubuntu-latest
target: x86_64-unknown-freebsd
skip_ssl: true
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
dependencies:
@@ -218,7 +156,8 @@ jobs:
gcc-aarch64-linux-gnu: ${{ matrix.dependencies.gcc-aarch64-linux-gnu || false }}
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
if: ${{ github.ref != 'refs/heads/main' }} # Never restore on main
with:
path: |
~/.cargo/bin/
@@ -227,39 +166,20 @@ jobs:
~/.cargo/git/db/
target/
# key won't match, will rely on restore-keys
key: ${{ runner.os }}-${{ matrix.target }}
key: cargo-check-${{ runner.os }}-${{ matrix.target }}
restore-keys: |
${{ runner.os }}-stable-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable-${{ matrix.target }}-
${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable--
cargo-check-${{ runner.os }}-${{ matrix.target }}-
- uses: dtolnay/rust-toolchain@stable
with:
target: ${{ matrix.target }}
- run: rustup toolchain install stable --target "${{ matrix.target }}"
- name: Setup Android NDK
if: ${{ matrix.target == 'aarch64-linux-android' }}
id: setup-ndk
uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
uses: nttld/setup-ndk@v1
with:
ndk-version: r27
add-to-path: true
- name: Append env conf to cargo
if: ${{ matrix.target == 'aarch64-linux-android' }}
env:
NDK_PATH: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
{
echo "[env]"
echo "CC_aarch64_linux_android = \"${NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang\""
echo "AR_aarch64_linux_android = \"${NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar\""
echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER = \"${NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang\""
} >> .cargo/config.toml
# - name: Prepare repository for redox compilation
# run: bash scripts/redox/uncomment-cargo.sh
# - name: Check compilation for Redox
@@ -268,12 +188,24 @@ jobs:
# command: check
# args: --ignore-rust-version
- name: Check compilation with threading
run: cargo check --target "${{ matrix.target }}" ${{ env.CARGO_ARGS_NO_SSL }} --features threading
- name: Check compilation
run: cargo check --target "${{ matrix.target }}" ${{ env.CARGO_ARGS_NO_SSL }}
env:
CC_aarch64_linux_android: ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang
AR_aarch64_linux_android: ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang
- name: Check compilation with ssl
if: ${{ !matrix.skip_ssl }}
run: cargo check --target "${{ matrix.target }}" ${{ env.CARGO_ARGS }}
- name: Save cache
if: ${{ github.ref == 'refs/heads/main' }} # only save on main
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-check-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml') }}-${{ hashFiles('Cargo.lock') }}-${{ github.sha }}
snippets_cpython:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
@@ -295,21 +227,18 @@ jobs:
- os: macos-latest
extra_test_args:
- '-u all'
env_polluting_tests:
- test_set
env_polluting_tests: []
skips: []
timeout: 50
- os: ubuntu-latest
extra_test_args:
- '-u all'
env_polluting_tests:
- test_set
env_polluting_tests: []
skips: []
timeout: 60
- os: windows-2025
extra_test_args: [] # TODO: Enable '-u all'
env_polluting_tests:
- test_set
env_polluting_tests: []
skips:
- test_rlcompleter
- test_pathlib # panic by surrogate chars
@@ -322,25 +251,17 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-
restore-keys: |
${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable--
# Windows runners randomly crashes, https://github.com/actions/cache/issues/1754
continue-on-error: true
toolchain: stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install macOS dependencies
uses: ./.github/actions/install-macos-deps
@@ -370,24 +291,8 @@ jobs:
- name: Run flaky MP CPython tests
run: |
for attempt in $(seq 1 5); do
echo "::group::Attempt ${attempt}"
set +e
target/release/rustpython -m test -j 1 ${{ join(matrix.extra_test_args, ' ') }} --slowest --fail-env-changed --timeout 600 -v ${{ env.FLAKY_MP_TESTS }}
status=$?
set -e
echo "::endgroup::"
if [ $status -eq 0 ]; then
exit 0
fi
done
exit 1
target/release/rustpython -m test -j 1 ${{ join(matrix.extra_test_args, ' ') }} --slowest --fail-env-changed --timeout 600 -v ${{ env.FLAKY_MP_TESTS }}
timeout-minutes: ${{ matrix.timeout }}
shell: bash
env:
RUSTPYTHON_SKIP_ENV_POLLUTERS: true
@@ -444,82 +349,12 @@ jobs:
shell: bash
run: python -I scripts/whats_left.py ${{ env.CARGO_ARGS }} --features jit
clippy:
name: clippy
runs-on: ${{ matrix.os }}
needs:
- determine_changes
permissions:
contents: read
if: |
needs.determine_changes.outputs.rust_code == 'true' ||
github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
os:
- macos-latest
- ubuntu-latest
- windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-
restore-keys: |
${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable--
# Windows runners randomly crashes, https://github.com/actions/cache/issues/1754
continue-on-error: true
- name: Clippy
run: cargo clippy --keep-going ${{ env.CARGO_ARGS }} --workspace --all-targets ${{ env.WORKSPACE_EXCLUDES }} -- -Dwarnings
cargo_shear:
name: cargo shear
runs-on: ubuntu-latest
needs:
- determine_changes
permissions:
contents: read
if: |
needs.determine_changes.outputs.rust_code == 'true' ||
github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: cargo-bins/cargo-binstall@aaa84a43aec4955a42c5ffc65d258961e39f276e # v1.19.1
- name: cargo shear
run: |
cargo binstall --no-confirm cargo-shear
cargo shear
lint:
name: Lint
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
issues: write
pull-requests: write
security-events: write # for zizmor
steps:
@@ -528,76 +363,55 @@ jobs:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: dtolnay/rust-toolchain@stable
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: stable
components: rustfmt
- uses: cargo-bins/cargo-binstall@113a77a4ce971c41332f2129c3d995df993cf746 # v1.17.8
- name: cargo shear
run: |
cargo binstall --no-confirm cargo-shear
cargo shear
- name: actionlint
uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1.72.0
uses: reviewdog/action-actionlint@0d952c597ef8459f634d7145b0b044a9699e5e43 # v1.71.0
- name: zizmor
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
- name: restore prek cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ github.ref != 'refs/heads/main' }} # never restore on main
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/prek
# TODO: Remove on 2026/06/02 when node24 is the default
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
package-manager-cache: false
node-version: "24"
- name: install prek
- name: prek
id: prek
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
with:
cache: false
show-verbose-logs: false
install-only: true
- name: prek run
run: prek run --show-diff-on-failure --color=always --all-files
- name: Get target CPython version
id: cpython-version
run: |
version=$(cat .python-version)
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Clone CPython
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: python/cpython
path: cpython
ref: "v${{ steps.cpython-version.outputs.version }}"
persist-credentials: false
- name: prek run (manual stage)
run: prek run --show-diff-on-failure --color=always --all-files --hook-stage manual
env:
CPYTHON_ROOT: ${{ github.workspace }}/cpython
continue-on-error: true
- name: save prek cache
if: ${{ github.ref == 'refs/heads/main' }} # only save on main
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/prek
- name: restore git permissions
if: ${{ !cancelled() }}
run: sudo chown -R "$(id -u):$(id -g)" .git
- name: reviewdog
if: ${{ !cancelled() }}
uses: reviewdog/action-suggester@aa38384ceb608d00f84b4690cacc83a5aba307ff # v1.24.0
uses: reviewdog/action-suggester@aa38384ceb608d00f84b4690cacc83a5aba307ff # 1.24.0
with:
level: warning
fail_level: error
cleanup: false
miri:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
@@ -611,23 +425,14 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: ${{ env.NIGHTLY_CHANNEL }}
components: miri
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}
restore-keys: |
${{ runner.os }}-
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run tests under miri
run: cargo +${{ env.NIGHTLY_CHANNEL }} miri test -p rustpython-vm -- miri_test
@@ -646,27 +451,17 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
components: clippy
toolchain: stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-
restore-keys: |
${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable--
${{ runner.os }}-
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo clippy
run: cargo clippy --keep-going --manifest-path=crates/wasm/Cargo.toml -- -Dwarnings
run: cargo clippy --manifest-path=crates/wasm/Cargo.toml -- -Dwarnings
- name: install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
@@ -677,29 +472,15 @@ jobs:
tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- run: python -m pip install -r requirements.txt
working-directory: ./wasm/tests
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
package-manager-cache: false
- name: Get npm cache directory
id: npm-cache-dir
shell: bash
run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT"
- name: Restore npm cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
# don't restore on main or release
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/release'
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: node-${{ runner.os }}-wasm-demo-
restore-keys: |
node-${{ runner.os }}-wasm-demo-
cache: "npm"
cache-dependency-path: "wasm/demo/package-lock.json"
- name: run test
run: |
driver_path="$(pwd)/../../geckodriver"
@@ -709,11 +490,8 @@ jobs:
env:
NODE_OPTIONS: "--openssl-legacy-provider"
working-directory: ./wasm/demo
- uses: mwilliamson/setup-wabt-action@427f2fdd70bc4dbc2e53c2eb4f19f66162d71bd2 # v4.0.0
with:
wabt-version: "1.0.36"
- uses: mwilliamson/setup-wabt-action@v3
with: { wabt-version: "1.0.36" }
- name: check wasm32-unknown without js
run: |
cd example_projects/wasm32_without_js/rustpython-without-js
@@ -723,7 +501,6 @@ jobs:
echo "ERROR: wasm32-unknown module expects imports from the host environment" >&2
fi
cargo run --release --manifest-path wasm-runtime/Cargo.toml rustpython-without-js/target/wasm32-unknown-unknown/debug/rustpython_without_js.wasm
- name: build notebook demo
if: github.ref == 'refs/heads/release'
run: |
@@ -733,24 +510,15 @@ jobs:
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@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
uses: peaceiris/actions-gh-pages@v4
env:
ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEMO_DEPLOY_KEY }}
PUBLISH_DIR: ./wasm/demo/dist
EXTERNAL_REPOSITORY: RustPython/demo
PUBLISH_BRANCH: master
- name: Save npm cache
# Save only on main or release
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: node-${{ runner.os }}-wasm-demo-${{ hashFiles('wasm/demo/package-lock.json') }}
wasm-wasi:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip:ci') }}
name: Run snippets and cpython tests on wasm-wasi
@@ -761,28 +529,17 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
target: wasm32-wasip1
toolchain: stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-
restore-keys: |
${{ runner.os }}-stable-wasm32-wasip1-${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable-wasm32-wasip1-
${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable--
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Setup Wasmer
uses: wasmerio/setup-wasmer@24b15c95293d23f89c68bd40dac76338f773e924 # v3.1
uses: wasmerio/setup-wasmer@v3
- name: Install clang
uses: ./.github/actions/install-linux-deps
@@ -790,44 +547,8 @@ jobs:
clang: true
- name: build rustpython
run: cargo build --release --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib,stdio,importlib,host_env --verbose
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"
cargo_doc:
needs:
- determine_changes
if: |
(
!contains(github.event.pull_request.labels.*.name, 'skip:ci') &&
needs.determine_changes.outputs.rust_code == 'true'
) || github.ref == 'refs/heads/main'
env:
RUST_BACKTRACE: full
name: cargo doc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-
restore-keys: |
${{ runner.os }}-stable--${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-stable--
- name: cargo doc
run: cargo doc --locked

View File

@@ -1,5 +1,3 @@
name: Periodic checks/tasks
on:
schedule:
- cron: "0 0 * * 6"
@@ -7,15 +5,15 @@ on:
push:
paths:
- .github/workflows/cron-ci.yaml
branches:
- main
pull_request:
paths:
- .github/workflows/cron-ci.yaml
name: Periodic checks/tasks
env:
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls-aws-lc,jit,host_env
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' # TODO: Remove on 2026/06/02
CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit,host_env
PYTHON_VERSION: "3.14.3"
jobs:
# codecov collects code coverage data from the rust tests, python snippets and python test suite.
@@ -31,32 +29,24 @@ jobs:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@b550161ef8a7bc4f2a671c0b03a18ac9ccedea1e # v2.79.1
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: actions/setup-python@v6.2.0
with:
tool: cargo-llvm-cov
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
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 --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls-aws-lc,jit,host_env
run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit,host_env
- 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
if: ${{ github.event_name != 'pull_request' }}
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
uses: codecov/codecov-action@v5
with:
files: ./codecov.lcov
@@ -71,15 +61,12 @@ jobs:
persist-credentials: true
- uses: dtolnay/rust-toolchain@stable
- name: build rustpython
run: cargo build --release --verbose
- name: collect tests data
run: cargo run --release extra_tests/jsontests.py
env:
RUSTPYTHONPATH: ${{ github.workspace }}/Lib
- name: upload tests data to the website
if: ${{ github.event_name != 'pull_request' }}
env:
@@ -109,19 +96,17 @@ jobs:
persist-credentials: true
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: build rustpython
run: cargo build --release --verbose
- name: Collect what is left data
run: |
chmod +x ./scripts/whats_left.py
./scripts/whats_left.py --features "ssl,sqlite" > whats_left.temp
env:
RUSTPYTHONPATH: ${{ github.workspace }}/Lib
- name: Upload data to the website
if: ${{ github.event_name != 'pull_request' }}
env:
@@ -172,20 +157,16 @@ jobs:
persist-credentials: true
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@v6.2.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- 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
@@ -198,7 +179,6 @@ jobs:
cd ..
mv reports/* .
rmdir reports
- name: upload benchmark data to the website
if: ${{ github.event_name != 'pull_request' }}
env:

View File

@@ -10,6 +10,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
PYTHON_VERSION: "3.14.3"
jobs:
check_deps:
permissions:
@@ -34,71 +37,46 @@ jobs:
# Checkout only Lib/ directory from PR head for accurate comparison
git checkout ${{ github.event.pull_request.head.sha }} -- Lib/
- name: Get target CPython version
id: cpython-version
run: |
version=$(cat .python-version)
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Checkout CPython
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: python/cpython
path: cpython
ref: "v${{ steps.cpython-version.outputs.version }}"
fetch-depth: 1
persist-credentials: false
run: |
git clone --depth 1 --branch "v${{ env.PYTHON_VERSION }}" https://github.com/python/cpython.git cpython
- name: Get changed Lib files
id: all-changed-files
run: |
# Get the list of changed files under Lib/
{
echo 'changed<<EOF'
git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} -- 'Lib/*.py' 'Lib/**/*.py'
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Parse changed files
id: changed-files
run: |
from os import environ
# Get the list of changed files under Lib/
changed=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} -- 'Lib/*.py' 'Lib/**/*.py' | head -50)
echo "Changed files:"
echo "$changed"
files = environ["FILES"]
modules = set()
for file in files.splitlines():
file = file.strip()
if file.startswith("Lib/test/"):
# Test files:
# Lib/test/test_pydoc.py -> test_pydoc
# Lib/test/test_pydoc/foo.py -> test_pydoc
module = file.removeprefix("Lib/test/").split("/")[0]
if not module.startswith("test_"):
# Extract unique module names
modules=""
for file in $changed; do
if [[ "$file" == Lib/test/* ]]; then
# Test files: Lib/test/test_pydoc.py -> test_pydoc, Lib/test/test_pydoc/foo.py -> test_pydoc
module=$(echo "$file" | sed -E 's|^Lib/test/||; s|\.py$||; s|/.*||')
# Skip non-test files in test/ (e.g., support.py, __init__.py)
if [[ ! "$module" == test_* ]]; then
continue
else:
# Lib files:
# Lib/foo.py -> foo
# Lib/foo/__init__.py -> foo
module = file.removeprefix("Lib/").split("/")[0]
module = module.split(".")[0]
modules.add(module)
fi
else
# Lib files: Lib/foo.py -> foo, Lib/foo/__init__.py -> foo
module=$(echo "$file" | sed -E 's|^Lib/||; s|/__init__\.py$||; s|\.py$||; s|/.*||')
fi
if [[ -n "$module" && ! " $modules " =~ " $module " ]]; then
modules="$modules $module"
fi
done
print(f"{modules=}")
output = " ".join(sorted(modules))
output_file = environ["GITHUB_OUTPUT"]
with open(output_file, mode="a", encoding="utf-8") as fd:
fd.write(f"modules={output}\n")
env:
FILES: ${{ steps.all-changed-files.outputs.changed }}
shell: python
modules=$(echo "$modules" | xargs) # trim whitespace
echo "Detected modules: $modules"
echo "modules=$modules" >> $GITHUB_OUTPUT
- name: Setup Python
if: steps.changed-files.outputs.modules != ''
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "${{ env.PYTHON_VERSION }}"
- name: Run deps check
if: steps.changed-files.outputs.modules != ''
@@ -114,7 +92,7 @@ jobs:
- name: Post comment
if: steps.deps-check.outputs.deps_output != ''
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
uses: marocchino/sticky-pull-request-comment@v3
with:
header: lib-deps-check
number: ${{ github.event.pull_request.number }}
@@ -131,7 +109,7 @@ jobs:
- name: Remove comment if no Lib changes
if: steps.changed-files.outputs.modules == ''
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
uses: marocchino/sticky-pull-request-comment@v3
with:
header: lib-deps-check
number: ${{ github.event.pull_request.number }}

View File

@@ -16,15 +16,11 @@ env:
X86_64_PC_WINDOWS_MSVC_OPENSSL_LIB_DIR: C:\Program Files\OpenSSL\lib\VC\x64\MD
X86_64_PC_WINDOWS_MSVC_OPENSSL_INCLUDE_DIR: C:\Program Files\OpenSSL\include
permissions: {}
jobs:
build:
runs-on: ${{ matrix.os }}
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
permissions:
contents: read
strategy:
matrix:
include:
@@ -56,7 +52,7 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
target: ${{ matrix.target }}
@@ -68,7 +64,7 @@ jobs:
libtool: true
- name: Build RustPython
run: cargo build --release --target=${{ matrix.target }} --verbose --no-default-features --features stdlib,stdio,importlib,encodings,sqlite,host_env,ssl-rustls-aws-lc,threading,jit
run: cargo build --release --target=${{ matrix.target }} --verbose --no-default-features --features stdlib,stdio,importlib,encodings,sqlite,host_env,ssl-rustls,threading,jit
- name: Rename Binary
run: cp target/${{ matrix.target }}/release/rustpython target/rustpython-release-${{ runner.os }}-${{ matrix.target }}
@@ -79,7 +75,7 @@ jobs:
if: runner.os == 'Windows'
- name: Upload Binary Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: rustpython-release-${{ runner.os }}-${{ matrix.target }}
path: target/rustpython-release-${{ runner.os }}-${{ matrix.target }}*
@@ -88,14 +84,12 @@ jobs:
runs-on: ubuntu-latest
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
targets: wasm32-wasip1
@@ -106,7 +100,7 @@ jobs:
run: cp target/wasm32-wasip1/release/rustpython.wasm target/rustpython-release-wasm32-wasip1.wasm
- name: Upload Binary Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: rustpython-release-wasm32-wasip1
path: target/rustpython-release-wasm32-wasip1.wasm
@@ -114,11 +108,11 @@ jobs:
- name: install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
package-manager-cache: false
- uses: mwilliamson/setup-wabt-action@427f2fdd70bc4dbc2e53c2eb4f19f66162d71bd2 # v4.0.0
- uses: mwilliamson/setup-wabt-action@febe2a12b7ccb999a6e5d953a8362a3b7ffcf148 # v3.2.0
with:
wabt-version: "1.0.30"
@@ -141,7 +135,7 @@ jobs:
- name: Deploy demo to Github Pages
if: ${{ github.repository == 'RustPython/RustPython' }}
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
with:
deploy_key: ${{ secrets.ACTIONS_DEMO_DEPLOY_KEY }}
publish_dir: ./wasm/demo/dist
@@ -153,8 +147,6 @@ jobs:
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
needs: [build, build-wasm]
permissions:
contents: write # for creating a release
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -195,7 +187,7 @@ jobs:
$PRERELEASE_ARG \
bin/rustpython-release-*
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
run: ${{ github.run_number }}
PRE_RELEASE_INPUT: ${{ github.event.inputs.pre-release }}

View File

@@ -1,77 +0,0 @@
name: Update Actions Caches
permissions:
contents: read
on:
workflow_dispatch:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
env:
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
CARGO_PROFILE_TEST_DEBUG: 0
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_RELEASE_DEBUG: 0
CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env,threading,jit --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher
jobs:
build-caches:
name: Build Caches
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- os: macos-latest
toolchain: stable
target: ""
- os: ubuntu-latest
toolchain: stable
target: ""
- os: windows-latest
toolchain: stable
target: ""
steps:
- name: Checkout RustPython main branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: RustPython/RustPython
ref: main
persist-credentials: false
- name: Setup Rust
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: ${{ matrix.toolchain }}
target: ${{ matrix.target }}
- name: Install macos dependencies
uses: ./.github/actions/install-macos-deps
with:
openssl: true
- name: Build dev cache # dev profile used by check & doc
run: cargo build --profile dev ${{ env.CARGO_ARGS }}
- name: Build test cache
run: cargo build --profile test ${{ env.CARGO_ARGS }}
- name: Build release cache
run: cargo build --profile release ${{ env.CARGO_ARGS }}
- name: Save cache
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-${{ matrix.toolchain }}-${{ matrix.target }}-${{ hashFiles('**/Cargo.toml') }}-${{ hashFiles('Cargo.lock') }}-${{ github.sha }}

View File

@@ -43,7 +43,7 @@ jobs:
- name: Generate docs
run: python crates/doc/generate.py
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: doc-db-${{ inputs.python-version }}-${{ matrix.os }}
path: "crates/doc/generated/*.json"
@@ -87,18 +87,19 @@ jobs:
OUTPUT_FILE='crates/doc/src/data.inc.rs'
# shellcheck disable=SC2016
{
echo '// This file was auto-generated by `.github/workflows/update-doc-db.yml`.'
echo "// CPython version: ${PYTHON_VERSION}"
echo '// spell-checker: disable'
echo ''
echo "pub static DB: phf::Map<&'static str, &'static str> = phf::phf_map! {"
cat crates/doc/generated/raw_entries.txt
echo '};'
} > "$OUTPUT_FILE"
echo -n '' > $OUTPUT_FILE
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
echo '// This file was auto-generated by `.github/workflows/update-doc-db.yml`.' >> $OUTPUT_FILE
echo "// CPython version: ${PYTHON_VERSION}" >> $OUTPUT_FILE
echo '// spell-checker: disable' >> $OUTPUT_FILE
echo '' >> $OUTPUT_FILE
echo "pub static DB: phf::Map<&'static str, &'static str> = phf::phf_map! {" >> $OUTPUT_FILE
cat crates/doc/generated/raw_entries.txt >> $OUTPUT_FILE
echo '};' >> $OUTPUT_FILE
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: doc-db-${{ inputs.python-version }}
path: "crates/doc/src/data.inc.rs"

View File

@@ -13,6 +13,7 @@ permissions:
issues: write
env:
PYTHON_VERSION: "v3.14.3"
ISSUE_ID: "6839"
jobs:
@@ -28,20 +29,13 @@ jobs:
sparse-checkout: |-
Lib
scripts/update_lib
.python-version
- name: Get target CPython version
id: cpython-version
run: |
version=$(cat rustpython/.python-version)
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Clone CPython
- name: Clone CPython ${{ env.PYTHON_VERSION }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: python/cpython
path: cpython
ref: "v${{ steps.cpython-version.outputs.version }}"
ref: ${{ env.PYTHON_VERSION }}
persist-credentials: false
sparse-checkout: |
Lib
@@ -62,14 +56,14 @@ jobs:
## Summary
Check \`scripts/update_lib\` for tools. As a note, the current latest Python version is \`${{ steps.cpython-version.outputs.version }}\`.
Check \`scripts/update_lib\` for tools. As a note, the current latest Python version is \`${{ env.PYTHON_VERSION }}\`.
Previous versions' issues as reference
- 3.13: #5529
<!--
Quick guideline for Copilot:
# Clone \`github.com/python/cpython\` \`${{ steps.cpython-version.outputs.version }}\` tag under RustPython working dir with depth 1 option; never 3.14.0 or 3.14.1 or 3.14.2
# Clone \`github.com/python/cpython\` \`${{ env.PYTHON_VERSION }}\` tag under RustPython working dir with depth 1 option; never 3.14.0 or 3.14.1 or 3.14.2
# Pick a library or test to update. Probably user give one.
# Run \`python3 scripts/update_lib quick <name>\`
# A commit is automatically created. push the commit.

View File

@@ -58,11 +58,11 @@ jobs:
comment_repo: ""
steps:
- name: Setup Scripts
uses: github/gh-aw/actions/setup@2c1a237d2048b0e2412e7d7528892ea1257840e2 # v0.74.4
uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5
with:
destination: /opt/gh-aw/actions
- name: Check workflow file timestamps
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_WORKFLOW_FILE: "upgrade-pylib.lock.yml"
with:
@@ -99,7 +99,7 @@ jobs:
secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
steps:
- name: Setup Scripts
uses: github/gh-aw/actions/setup@2c1a237d2048b0e2412e7d7528892ea1257840e2 # v0.74.4
uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5
with:
destination: /opt/gh-aw/actions
- name: Checkout repository
@@ -114,7 +114,7 @@ jobs:
run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
# Cache configuration from frontmatter processed below
- name: Cache (cpython-lib-${{ env.PYTHON_VERSION }})
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
key: cpython-lib-${{ env.PYTHON_VERSION }}
path: cpython
@@ -135,7 +135,7 @@ jobs:
id: checkout-pr
if: |
github.event.pull_request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
with:
@@ -147,7 +147,7 @@ jobs:
await main();
- name: Generate agentic run info
id: generate_aw_info
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const fs = require('fs');
@@ -201,7 +201,7 @@ jobs:
run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.16.4
- name: Determine automatic lockdown mode for GitHub MCP server
id: determine-automatic-lockdown
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
@@ -484,7 +484,7 @@ jobs:
}
GH_AW_MCP_CONFIG_EOF
- name: Generate workflow overview
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs');
@@ -508,11 +508,10 @@ jobs:
cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT"
<system>
GH_AW_PROMPT_EOF
{
cat "/opt/gh-aw/prompts/xpia.md"
cat "/opt/gh-aw/prompts/temp_folder_prompt.md"
cat "/opt/gh-aw/prompts/markdown.md"
cat << 'GH_AW_PROMPT_EOF'
cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT"
cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT"
cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT"
cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
<safe-outputs>
<description>GitHub API Access Instructions</description>
<important>
@@ -570,15 +569,14 @@ jobs:
</github-context>
GH_AW_PROMPT_EOF
cat << 'GH_AW_PROMPT_EOF'
cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
</system>
GH_AW_PROMPT_EOF
cat << 'GH_AW_PROMPT_EOF'
cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT"
{{#runtime-import .github/workflows/upgrade-pylib.md}}
GH_AW_PROMPT_EOF
} >> "$GH_AW_PROMPT"
- name: Substitute placeholders
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_ENV_ISSUE_ID: ${{ env.ISSUE_ID }}
@@ -612,7 +610,7 @@ jobs:
}
});
- name: Interpolate variables and render templates
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
GH_AW_ENV_ISSUE_ID: ${{ env.ISSUE_ID }}
@@ -692,7 +690,7 @@ jobs:
bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
- name: Redact secrets in logs
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
@@ -707,14 +705,14 @@ jobs:
SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Safe Outputs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: safe-output
path: ${{ env.GH_AW_SAFE_OUTPUTS }}
if-no-files-found: warn
- name: Ingest agent output
id: collect_output
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
GH_AW_ALLOWED_DOMAINS: "*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,binstar.org,bootstrap.pypa.io,conda.anaconda.org,conda.binstar.org,crates.io,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,github.com,host.docker.internal,index.crates.io,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,pip.pypa.io,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.npmjs.org,repo.anaconda.com,repo.continuum.io,s.symcb.com,s.symcd.com,security.ubuntu.com,sh.rustup.rs,static.crates.io,static.rust-lang.org,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com"
@@ -728,13 +726,13 @@ jobs:
await main();
- name: Upload sanitized agent output
if: always() && env.GH_AW_AGENT_OUTPUT
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: agent-output
path: ${{ env.GH_AW_AGENT_OUTPUT }}
if-no-files-found: warn
- name: Upload engine output files
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: agent_outputs
path: |
@@ -743,7 +741,7 @@ jobs:
if-no-files-found: ignore
- name: Parse agent logs for step summary
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
with:
@@ -754,7 +752,7 @@ jobs:
await main();
- name: Parse MCP gateway logs for step summary
if: always()
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
@@ -774,7 +772,7 @@ jobs:
- name: Upload agent artifacts
if: always()
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: agent-artifacts
path: |
@@ -806,7 +804,7 @@ jobs:
total_count: ${{ steps.missing_tool.outputs.total_count }}
steps:
- name: Setup Scripts
uses: github/gh-aw/actions/setup@2c1a237d2048b0e2412e7d7528892ea1257840e2 # v0.74.4
uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5
with:
destination: /opt/gh-aw/actions
- name: Download agent output artifact
@@ -822,7 +820,7 @@ jobs:
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV"
- name: Process No-Op Messages
id: noop
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_NOOP_MAX: 1
@@ -836,7 +834,7 @@ jobs:
await main();
- name: Record Missing Tool
id: missing_tool
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Upgrade Python Library"
@@ -849,7 +847,7 @@ jobs:
await main();
- name: Handle Agent Failure
id: handle_agent_failure
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Upgrade Python Library"
@@ -867,7 +865,7 @@ jobs:
await main();
- name: Handle No-Op Message
id: handle_noop_message
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Upgrade Python Library"
@@ -884,7 +882,7 @@ jobs:
await main();
- name: Handle Create Pull Request Error
id: handle_create_pr_error
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_WORKFLOW_NAME: "Upgrade Python Library"
@@ -898,7 +896,7 @@ jobs:
await main();
- name: Update reaction comment with completion status
id: conclusion
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }}
@@ -927,7 +925,7 @@ jobs:
success: ${{ steps.parse_results.outputs.success }}
steps:
- name: Setup Scripts
uses: github/gh-aw/actions/setup@2c1a237d2048b0e2412e7d7528892ea1257840e2 # v0.74.4
uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5
with:
destination: /opt/gh-aw/actions
- name: Download agent artifacts
@@ -948,7 +946,7 @@ jobs:
run: |
echo "Agent output-types: $AGENT_OUTPUT_TYPES"
- name: Setup threat detection
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
WORKFLOW_NAME: "Upgrade Python Library"
WORKFLOW_DESCRIPTION: "Pick an out-of-sync Python library from the todo list and upgrade it\nby running `scripts/update_lib quick`, then open a pull request."
@@ -1001,7 +999,7 @@ jobs:
XDG_CONFIG_HOME: /home/runner
- name: Parse threat detection results
id: parse_results
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
@@ -1010,7 +1008,7 @@ jobs:
await main();
- name: Upload threat detection log
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: threat-detection.log
path: /tmp/gh-aw/threat-detection/detection.log
@@ -1039,7 +1037,7 @@ jobs:
process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
steps:
- name: Setup Scripts
uses: github/gh-aw/actions/setup@2c1a237d2048b0e2412e7d7528892ea1257840e2 # v0.74.4
uses: github/gh-aw/actions/setup@48d8fdfddc8cad854ac0c70ceb573f09fb8f9c9b # v0.62.5
with:
destination: /opt/gh-aw/actions
- name: Download agent output artifact
@@ -1081,7 +1079,7 @@ jobs:
echo "Git configured with standard GitHub Actions identity"
- name: Process Safe Outputs
id: process_safe_outputs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":false,\"expires\":30,\"labels\":[\"pylib-sync\"],\"max\":1,\"max_patch_size\":1024,\"title_prefix\":\"Update \"},\"missing_data\":{},\"missing_tool\":{}}"

View File

@@ -52,7 +52,7 @@ cache:
- cpython-lib-
env:
PYTHON_VERSION: "v3.14.4"
PYTHON_VERSION: "v3.14.3"
ISSUE_ID: "6839"
---

14
.github/zizmor.yml vendored
View File

@@ -1,14 +0,0 @@
rules:
unpinned-uses:
config:
policies:
# dtolnay/rust-toolchain is a trusted action that uses lightweight branch
# refs (@stable, @nightly, etc.) by design. Pinning to a hash would break
# the intended usage pattern.
# We can remove this once https://github.com/dtolnay/rust-toolchain/issues/180 is resolved
dtolnay/rust-toolchain: any
# dtolnay/rust-toolchain handles component installation, target addition, and
# override configuration beyond what a bare `rustup` invocation provides.
# See: https://github.com/zizmorcore/zizmor/issues/1817
superfluous-actions:
disable: true

3
.gitignore vendored
View File

@@ -10,6 +10,7 @@ __pycache__/
wasm-pack.log
.idea/
.envrc
.python-version
flame-graph.html
flame.txt
@@ -27,4 +28,4 @@ Lib/site-packages/*
Lib/test/data/*
!Lib/test/data/README
cpython/
.claude/scheduled_tasks.lock

View File

@@ -10,7 +10,7 @@ repos:
priority: 0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
rev: v0.15.7
hooks:
- id: ruff-format
priority: 0
@@ -40,30 +40,17 @@ repos:
types: [rust]
priority: 0
- id: generate-rs-opcode-metadata
name: generate rust opcode metadata
entry: python tools/opcode_metadata/generate_rs_opcode_metadata.py
files: '^(crates/compiler-core/src/bytecode/instruction\.rs|tools/opcode_metadata/*)$'
- id: generate-opcode-metadata
name: generate opcode metadata
entry: python scripts/generate_opcode_metadata.py
files: '^(crates/compiler-core/src/bytecode/instruction\.rs|scripts/generate_opcode_metadata\.py)$'
pass_filenames: false
language: system
require_serial: true
priority: 1 # so rustfmt runs first
stages:
- manual
- id: generate-py-opcode-metadata
name: generate python opcode metadata
entry: python tools/opcode_metadata/generate_py_opcode_metadata.py
files: '^(crates/compiler-core/src/bytecode/instruction\.rs|tools/opcode_metadata/*)$'
pass_filenames: false
language: system
require_serial: true
priority: 1 # so rustfmt runs first
stages:
- manual
- repo: https://github.com/streetsidesoftware/cspell-cli
rev: v10.0.0
rev: v9.7.0
hooks:
- id: cspell
types: [rust]
@@ -77,7 +64,7 @@ repos:
priority: 0
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.8.3
rev: v3.8.1
hooks:
- id: prettier
files: '^wasm/.*$'

View File

@@ -1 +0,0 @@
3.14.5

View File

@@ -38,12 +38,6 @@ RustPython is a Python 3 interpreter written in Rust, implementing Python 3.14.0
- Always ask the user before performing any git operations that affect the remote repository
- Commits can be created locally when requested, but pushing and PR creation require explicit approval
**CRITICAL: Pre-commit Checks**
- Before creating ANY commit, you MUST run `prek run --all-files` (or `pre-commit run --all-files`) AND the full test suite. Both must pass — do not commit if either fails.
- Test commands are documented in the [Testing](#testing) section below. At minimum run `cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher`; if the change touches `extra_tests/snippets/` run `pytest -v` there too, and if it touches `Lib/` or interpreter behavior, run the relevant `cargo run --release -- -m test <module>` modules.
- If a hook auto-fixes files (e.g. `ruff-format`, `rustfmt`), re-stage the fixes, re-run `prek` until it reports a clean pass, then re-run the tests, then commit.
- NEVER bypass these checks with `--no-verify`, `--no-gpg-sign`, or by skipping tests "because the change is small". If a hook or test fails, fix the underlying issue and create a new commit — do not amend or force the failing commit through.
## Important Development Notes
### Running Python Code
@@ -87,35 +81,6 @@ The `Lib/` directory contains Python standard library files copied from the CPyt
- `unittest.skip("TODO: RustPython <reason>")`
- `unittest.expectedFailure` with `# TODO: RUSTPYTHON <reason>` comment
#### Choosing the right marker
When marking a test that fails on RustPython, prefer one of the following forms:
```python
@unittest.expectedFailure # TODO: RUSTPYTHON; <reason>
# or
@unittest.expectedFailureIf(<condition>, "TODO: RUSTPYTHON; <reason>")
```
If the test would crash the interpreter (segfault, Rust panic, abort, infinite loop), use `skip` instead so the rest of the suite can still run:
```python
@unittest.skip("TODO: RUSTPYTHON; <reason>")
# or
@unittest.skipIf(<condition>, "TODO: RUSTPYTHON; <reason>")
```
**When to use which:**
- **Prefer `expectedFailure` / `expectedFailureIf`** by default. The test body still runs, so if RustPython is later fixed, the unexpected pass surfaces immediately and the decorator can be removed. Use the conditional `*If` form when the failure is environment-specific (e.g., a platform or build flag).
- **Use `skip` / `skipIf` only when running the test would take down the test process** — segfaults, Rust panics, aborts, or hangs that block subsequent tests. Skipping keeps the suite usable; `expectedFailure` cannot help here, because the test body still executes.
To find WIP entries that are partly modified and may need follow-up:
```bash
grep -d recurse 'TODO: RUSTPYTHON' Lib/test/
```
### Clean Build
When you modify bytecode instructions, a full clean is required:
@@ -164,7 +129,6 @@ Run `./scripts/whats_left.py` to get a list of unimplemented methods, which is h
- Do not delete or rewrite existing comments unless they are factually wrong or directly contradict the new code.
- Do not add decorative section separators (e.g. `// -----------`, `// ===`, `/* *** */`). Use `///` doc-comments or short `//` comments only when they add value.
- Do not put `///` doc comments on items annotated with `#[pyattr]`, `#[pyclass]`, or `#[pyfunction]`. The derive macros pull authoritative docstrings from CPython via the `rustpython-doc` crate; a Rust doc comment overrides that source, and on `#[pyattr]` it is silently dropped.
#### Avoid Duplicate Code in Branches
@@ -294,14 +258,9 @@ See DEVELOPMENT.md "CPython Version Upgrade Checklist" section.
- Document that it requires PEP 695 support
- Focus on tests that can be fixed through Rust code changes only
## CI Workflows
If you modify any file under `.github/workflows/`, the change must pass a [zizmor](https://docs.zizmor.sh/) scan in CI.
## 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/)
- [How to update test files](https://github.com/RustPython/RustPython/wiki/How-to-update-test-files#checkout-cpython-source-code-initial-setup) — guide for syncing test cases from upstream CPython into the `Lib/` directory

2037
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,8 +10,7 @@ repository.workspace = true
license.workspace = true
[features]
capi = ["dep:rustpython-capi", "threading"]
default = ["threading", "stdlib", "stdio", "importlib", "ssl-rustls-aws-lc", "host_env"]
default = ["threading", "stdlib", "stdio", "importlib", "ssl-rustls", "host_env"]
host_env = ["rustpython-vm/host_env", "rustpython-stdlib?/host_env"]
importlib = ["rustpython-vm/importlib"]
encodings = ["rustpython-vm/encodings"]
@@ -22,35 +21,30 @@ freeze-stdlib = ["stdlib", "rustpython-vm/freeze-stdlib", "rustpython-pylib?/fre
jit = ["rustpython-vm/jit"]
threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"]
sqlite = ["rustpython-stdlib/sqlite"]
ssl = ["host_env"]
ssl = []
ssl-rustls = ["ssl", "rustpython-stdlib/ssl-rustls"]
ssl-rustls-aws-lc = ["ssl-rustls", "dep:rustls", "rustls/aws_lc_rs"]
ssl-rustls-aws-lc-fips = ["ssl-rustls-aws-lc", "rustls/fips"]
ssl-openssl = ["ssl", "rustpython-stdlib/ssl-openssl"]
ssl-openssl-vendor = ["ssl-openssl", "rustpython-stdlib/ssl-openssl-vendor"]
ssl-vendor = ["ssl-openssl", "rustpython-stdlib/ssl-vendor"]
tkinter = ["rustpython-stdlib/tkinter"]
[build-dependencies]
winresource = "0.1"
[dependencies]
rustpython-capi = { workspace = true, optional = true }
rustpython-compiler = { workspace = true }
rustpython-pylib = { workspace = true, optional = true }
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
rustpython-vm = { workspace = true, features = ["compiler", "gc"] }
cfg-if = { workspace = true }
log = { workspace = true }
flame = { workspace = true, optional = true }
lexopt = "0.3"
dirs = "6"
dirs = { package = "dirs-next", version = "2.0" }
env_logger = "0.11"
flamescope = { version = "0.1.2", optional = true }
rustls = { workspace = true, optional = true }
rustls-graviola = { workspace = true, optional = true }
[target.'cfg(windows)'.dependencies]
libc = { workspace = true }
@@ -59,7 +53,7 @@ rustyline = { workspace = true }
[dev-dependencies]
criterion = { workspace = true }
pyo3 = { workspace = true, features = ["auto-initialize"] }
pyo3 = { version = "0.28.2", features = ["auto-initialize"] }
rustpython-stdlib = { workspace = true }
ruff_python_parser = { workspace = true }
@@ -75,17 +69,6 @@ harness = false
name = "rustpython"
path = "src/main.rs"
[[example]]
name = "custom_tls_providers"
path = "examples/custom_tls_providers.rs"
required-features = [
"rustls-graviola",
"rustls/ring",
"rustpython-pylib/freeze-stdlib",
"rustpython-stdlib/ssl-rustls",
"rustpython-vm/freeze-stdlib",
]
[profile.dev.package."*"]
opt-level = 3
@@ -118,7 +101,7 @@ opt-level = 3
lto = "thin"
[patch.crates-io]
parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" }
# parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" }
# REDOX START, Uncomment when you want to compile/check with redoxer
# REDOX END
@@ -153,17 +136,15 @@ exclude = ["pymath"]
version = "0.5.0"
authors = ["RustPython Team"]
edition = "2024"
rust-version = "1.95.0"
rust-version = "1.93.0"
repository = "https://github.com/RustPython/RustPython"
license = "MIT"
[workspace.dependencies]
rustpython-capi = { path = "crates/capi", version = "0.5.0" }
rustpython-compiler-core = { path = "crates/compiler-core", version = "0.5.0" }
rustpython-compiler = { path = "crates/compiler", version = "0.5.0" }
rustpython-codegen = { path = "crates/codegen", version = "0.5.0" }
rustpython-common = { path = "crates/common", version = "0.5.0" }
rustpython-host_env = { path = "crates/host_env", version = "0.5.0" }
rustpython-derive = { path = "crates/derive", version = "0.5.0" }
rustpython-derive-impl = { path = "crates/derive-impl", version = "0.5.0" }
rustpython-jit = { path = "crates/jit", version = "0.5.0" }
@@ -189,141 +170,70 @@ ruff_source_file = { package = "rustpython-ruff_source_file", version = "0.15.8"
# ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] }
phf = { version = "0.13.1", default-features = false, features = ["macros"]}
adler32 = "1.2.0"
approx = "0.5.1"
ahash = "0.8.12"
ascii = "1.1"
base64 = "0.22"
blake2 = "0.10.4"
bitflags = "2.11.0"
bitflagset = "0.0.3"
bstr = "1"
bzip2 = "0.6"
chrono = { version = "0.4.44", default-features = false, features = ["clock", "std"] }
console_error_panic_hook = "0.1"
cfg-if = "1.0"
chrono = { version = "0.4.44", default-features = false, features = ["clock", "oldtime", "std"] }
constant_time_eq = "0.4"
cranelift = "0.131.2"
cranelift-jit = "0.131.2"
cranelift-module = "0.131.0"
crc32fast = "1.3.2"
criterion = { version = "0.8", features = ["html_reports"] }
crossbeam-utils = "0.8.21"
csv-core = "0.1.11"
digest = "0.10.7"
dns-lookup = "3.0"
dyn-clone = "1.0.10"
exitcode = "1.1.2"
flame = "0.2.2"
flamer = "0.5"
flate2 = { version = "1.1.9", default-features = false }
# Bump only when the openssl crate bumps it
foreign-types-shared = "0.1"
gethostname = "1.0.2"
getrandom = { version = "0.3", features = ["std"] }
glob = "0.3"
half = "2"
hex = "0.4.3"
hexf-parse = "0.2.1"
hmac = "0.12"
indexmap = { version = "2.14.0", features = ["std"] }
insta = "1.47"
indexmap = { version = "2.13.0", features = ["std"] }
insta = "1.46"
itertools = "0.14.0"
is-macro = "0.3.7"
js-sys = "0.3"
junction = "1.4.2"
lexical-parse-float = "1.0.6"
libc = "0.2.186"
libc = "0.2.183"
libffi = "5"
libloading = "0.9"
liblzma = "0.4"
liblzma-sys = "0.4"
libsqlite3-sys = "0.37"
libz-rs-sys = "0.6"
lock_api = "0.4"
log = "0.4.29"
lz4_flex = "0.13"
nix = { version = "0.31", features = ["fs", "user", "process", "term", "time", "signal", "ioctl", "socket", "sched", "zerocopy", "dir", "hostname", "net", "poll"] }
mac_address = "1.1.3"
nix = { version = "0.30", features = ["fs", "user", "process", "term", "time", "signal", "ioctl", "socket", "sched", "zerocopy", "dir", "hostname", "net", "poll"] }
malachite-bigint = "0.9.1"
malachite-q = "0.9.1"
malachite-base = "0.9.1"
md-5 = "0.10.1"
memchr = "2.8.0"
memmap2 = "0.9.10"
mt19937 = "<=3.2" # upgrade it once rand is upgraded
num-complex = "0.4.6"
num-integer = "0.1.46"
num-traits = "0.2"
num_cpus = "1.17.0"
num_enum = { version = "0.7", default-features = false }
oid-registry = "0.8"
openssl = "0.10.80"
openssl-sys = "0.9.110"
openssl-probe = "0.2.1"
optional = "0.5"
parking_lot = "0.12.3"
paste = "1.0.15"
pbkdf2 = "0.12"
pem-rfc7468 = "1.0"
pkcs8 = "0.11"
proc-macro2 = "1.0.105"
psm = "0.1"
pymath = { version = "0.2.0", features = ["mul_add", "malachite-bigint", "complex"] }
pyo3 = "0.28"
quote = "1.0.45"
radium = "1.1.1"
rand = "0.9"
rand_core = { version = "0.9", features = ["os_rng"] }
rapidhash = "4.4.1"
result-like = "0.5.0"
rustix = { version = "1.1", features = ["event", "param", "system"] }
rustls = { version = "0.23.39", default-features = false }
rustls-graviola = "0.3"
rustls-native-certs = "0.8"
rustls-pemfile = "2.2"
rustls-platform-verifier = "0.7"
rustyline = "18"
rustix = { version = "1.1", features = ["event"] }
rustyline = "17.0.1"
serde = { package = "serde_core", version = "1.0.225", default-features = false, features = ["alloc"] }
schannel = "0.1.29"
scoped-tls = "1"
scopeguard = "1"
serde-wasm-bindgen = "0.6.5"
sha-1 = "0.10.0"
sha2 = "0.10.2"
sha3 = "0.10.1"
siphasher = "1"
socket2 = "0.6.3"
static_assertions = "1.1"
strum = "0.28"
strum_macros = "0.28"
syn = "2"
syn-ext = "0.5.0"
system-configuration = "0.7.0"
tcl-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0" }
textwrap = { version = "0.16.2", default-features = false }
termios = "0.3.3"
thiserror = "2.0"
timsort = "0.1.2"
tk-sys = { git = "https://github.com/arihant2math/tkinter.git", tag = "v0.2.0" }
icu_casemap = "2"
icu_locale = "2"
icu_properties = "2"
icu_normalizer = "2"
uuid = "1.23.1"
ucd = "0.1.1"
unicode-casing = "0.1.1"
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 = "2.0.0"
unicode-bidi-mirroring = "0.4"
widestring = "1.2.0"
windows-sys = "0.61.2"
wasm-bindgen = "0.2.106"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"
webpki-roots = "1.0"
which = "8"
x509-cert = "0.2.5"
x509-parser = "0.18"
xml = "1.2"
writeable = "0.6"
# Lints
@@ -331,61 +241,13 @@ writeable = "0.6"
unsafe_code = "allow"
unsafe_op_in_unsafe_fn = "deny"
elided_lifetimes_in_paths = "warn"
unreachable_pub = "warn"
[workspace.lints.clippy]
correctness = { level = "warn", priority = -2 }
suspicious = { level = "warn", priority = -2 }
perf = { level = "warn", priority = -2 }
style = { level = "warn", priority = -2 }
complexity = { level = "warn", priority = -2 }
# pedantic = { level = "warn", priority = -2 } # TODO: Enable this
missing_errors_doc = "allow" # Too many errors. No auto-fix available
missing_panics_doc = "allow" # Too many errors. No auto-fix available
match_same_arms = "allow" # Not always more readable
if_not_else = "allow" # Not always more readable
single_match_else = "allow"
similar_names = "allow"
# restriction lints
alloc_instead_of_core = "warn"
cfg_not_test = "warn"
redundant_test_prefix = "warn"
std_instead_of_alloc = "warn"
std_instead_of_core = "warn"
tests_outside_test_module = "warn"
# nursery lints to enforce gradually
debug_assert_with_mut_call = "warn"
derive_partial_eq_without_eq = "warn"
imprecise_flops = "warn"
or_fun_call = "warn"
redundant_clone = "warn"
search_is_some = "warn"
single_option_map = "warn"
trait_duplication_in_bounds = "warn"
unused_peekable = "warn"
unused_rounding = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
# pedantic lints to enforce gradually
cloned_instead_of_copied = "warn"
collapsible_else_if = "warn"
comparison_chain = "warn"
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
filter_map_next = "warn"
flat_map_option = "warn"
format_collect = "warn"
from_iter_instead_of_collect = "warn"
inconsistent_struct_constructor = "warn"
inefficient_to_string = "warn"
manual_is_variant_and = "warn"
map_unwrap_or = "warn"
must_use_candidate = "warn"
redundant_else = "warn"
uninlined_format_args = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
perf = "warn"
style = "warn"
complexity = "warn"
suspicious = "warn"
correctness = "warn"

View File

@@ -1,28 +1,4 @@
# Contributing to RustPython
Contributions are more than welcome, and in many cases we are happy to guide
contributors through PRs or on [**Discord**](https://discord.gg/vru8NypEhv).
## Finding ways to help
We label issues that would be good for a first time contributor as [`good first issue`](https://github.com/RustPython/RustPython/issues?q=label%3A%22good+first+issue%22+is%3Aissue+is%3Aopen+).
Also checkout the [issue tracker](https://github.com/RustPython/RustPython/issues) for all open issues.
You can enhance CPython compatibility by increasing our unittest coverage, you can see [This pinned issue](https://github.com/RustPython/RustPython/issues/6839) to see which libs and tests need be updated to our current supported python version.
Another approach is to checkout the source code: builtin functions and object
methods are often the simplest and easiest way to contribute.
You can also simply run `python -I scripts/whats_left.py` to assist in finding any unimplemented method.
## Use of AI
We **require all use of AI in contributions to follow our
[AI Policy](https://github.com/RustPython/.github/blob/main/AI_POLICY.md)**.
If your contribution does not follow the policy, it will be closed.
## RustPython Development Guide and Tips
# RustPython Development Guide and Tips
RustPython attracts developers with interest and experience in Rust, Python,
or WebAssembly. Whether you are familiar with Rust, Python, or

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 RustPython Team
Copyright (c) 2025 RustPython Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -168,13 +168,6 @@ class Logcat:
# message.
message = message.replace(b"\x00", b"\xc0\x80")
# On API level 30 and higher, Logcat will strip any number of leading
# newlines. This is visible in all `logcat` modes, even --binary. Work
# around this by adding a leading space, which shouldn't make any
# difference to the log's usability.
if message.startswith(b"\n"):
message = b" " + message
with self._lock:
now = time()
self._bucket_level += (

2
Lib/_opcode_metadata.py generated vendored
View File

@@ -1,4 +1,4 @@
# This file is generated by tools/opcode_metadata/generate_py_opcode_metadata.py
# This file is generated by scripts/generate_opcode_metadata.py
# for RustPython bytecode format (CPython 3.14 compatible opcode numbers).
# Do not edit!

55
Lib/annotationlib.py vendored
View File

@@ -47,7 +47,6 @@ _SLOTS = (
"__cell__",
"__owner__",
"__stringifier_dict__",
"__resolved_str_cache__",
)
@@ -95,7 +94,6 @@ class ForwardRef:
# value later.
self.__code__ = None
self.__ast_node__ = None
self.__resolved_str_cache__ = None
def __init_subclass__(cls, /, *args, **kwds):
raise TypeError("Cannot subclass ForwardRef")
@@ -115,7 +113,7 @@ class ForwardRef:
"""
match format:
case Format.STRING:
return self.__resolved_str__
return self.__forward_arg__
case Format.VALUE:
is_forwardref_format = False
case Format.FORWARDREF:
@@ -260,24 +258,6 @@ class ForwardRef:
"Attempted to access '__forward_arg__' on an uninitialized ForwardRef"
)
@property
def __resolved_str__(self):
# __forward_arg__ with any names from __extra_names__ replaced
# with the type_repr of the value they represent
if self.__resolved_str_cache__ is None:
resolved_str = self.__forward_arg__
names = self.__extra_names__
if names:
visitor = _ExtraNameFixer(names)
ast_expr = ast.parse(resolved_str, mode="eval").body
node = visitor.visit(ast_expr)
resolved_str = ast.unparse(node)
self.__resolved_str_cache__ = resolved_str
return self.__resolved_str_cache__
@property
def __forward_code__(self):
if self.__code__ is not None:
@@ -341,7 +321,7 @@ class ForwardRef:
extra.append(", is_class=True")
if self.__owner__ is not None:
extra.append(f", owner={self.__owner__!r}")
return f"ForwardRef({self.__resolved_str__!r}{''.join(extra)})"
return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
_Template = type(t"")
@@ -377,7 +357,6 @@ class _Stringifier:
self.__cell__ = cell
self.__owner__ = owner
self.__stringifier_dict__ = stringifier_dict
self.__resolved_str_cache__ = None # Needed for ForwardRef
def __convert_to_ast(self, other):
if isinstance(other, _Stringifier):
@@ -940,7 +919,7 @@ def get_annotations(
does not exist, the __annotate__ function is called. The
FORWARDREF format uses __annotations__ if it exists and can be
evaluated, and otherwise falls back to calling the __annotate__ function.
The STRING format tries __annotate__ first, and falls back to
The SOURCE format tries __annotate__ first, and falls back to
using __annotations__, stringified using annotations_to_string().
This function handles several details for you:
@@ -1058,26 +1037,13 @@ def get_annotations(
obj_globals = obj_locals = unwrap = None
if unwrap is not None:
# Use an id-based visited set to detect cycles in the __wrapped__
# and functools.partial.func chain (e.g. f.__wrapped__ = f).
# On cycle detection we stop and use whatever __globals__ we have
# found so far, mirroring the approach of inspect.unwrap().
_seen_ids = {id(unwrap)}
while True:
if hasattr(unwrap, "__wrapped__"):
candidate = unwrap.__wrapped__
if id(candidate) in _seen_ids:
break
_seen_ids.add(id(candidate))
unwrap = candidate
unwrap = unwrap.__wrapped__
continue
if functools := sys.modules.get("functools"):
if isinstance(unwrap, functools.partial):
candidate = unwrap.func
if id(candidate) in _seen_ids:
break
_seen_ids.add(id(candidate))
unwrap = candidate
unwrap = unwrap.func
continue
break
if hasattr(unwrap, "__globals__"):
@@ -1184,14 +1150,3 @@ def _get_dunder_annotations(obj):
if not isinstance(ann, dict):
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
return ann
class _ExtraNameFixer(ast.NodeTransformer):
"""Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation"""
def __init__(self, extra_names):
self.extra_names = extra_names
def visit_Name(self, node: ast.Name):
if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel:
node = ast.Name(id=type_repr(new_name))
return node

12
Lib/argparse.py vendored
View File

@@ -149,10 +149,6 @@ def _copy_items(items):
return copy.copy(items)
def _identity(value):
return value
# ===============
# Formatting Help
# ===============
@@ -204,7 +200,7 @@ class HelpFormatter(object):
self._decolor = decolor
else:
self._theme = get_theme(force_no_color=True).argparse
self._decolor = _identity
self._decolor = lambda text: text
# ===============================
# Section and indentation methods
@@ -1907,7 +1903,9 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
self._subparsers = None
# register types
self.register('type', None, _identity)
def identity(string):
return string
self.register('type', None, identity)
# add help argument if necessary
# (using explicit default to override global argument_default)
@@ -2678,7 +2676,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
if value not in choices:
args = {'value': str(value),
'choices': ', '.join(repr(str(choice)) for choice in action.choices)}
'choices': ', '.join(map(str, action.choices))}
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
if self.suggest_on_error and isinstance(value, str):

View File

@@ -17,10 +17,6 @@ class defaultdict(dict):
val = self.default_factory()
else:
raise KeyError(key)
# CPython parity: a recursive __missing__ via factory() may have
# already populated key; preserve that value instead of overwriting.
if key in self:
return self[key]
self[key] = val
return val

17
Lib/configparser.py vendored
View File

@@ -315,15 +315,12 @@ class ParsingError(Error):
def append(self, lineno, line):
self.errors.append((lineno, line))
self.message += f'\n\t[line {lineno:2d}]: {line!r}'
self.message += '\n\t[line %2d]: %s' % (lineno, repr(line))
def combine(self, others):
messages = [self.message]
for other in others:
for lineno, line in other.errors:
self.errors.append((lineno, line))
messages.append(f'\n\t[line {lineno:2d}]: {line!r}')
self.message = "".join(messages)
for error in other.errors:
self.append(*error)
return self
@staticmethod
@@ -616,9 +613,7 @@ class RawConfigParser(MutableMapping):
\] # ]
"""
_OPT_TMPL = r"""
(?P<option> # very permissive!
(?:(?!{delim})\S)* # non-delimiter non-whitespace
(?:\s+(?:(?!{delim})\S)+)*) # optionally more words
(?P<option>.*?) # very permissive!
\s*(?P<vi>{delim})\s* # any number of space/tab,
# followed by any of the
# allowed delimiters,
@@ -626,9 +621,7 @@ class RawConfigParser(MutableMapping):
(?P<value>.*)$ # everything up to eol
"""
_OPT_NV_TMPL = r"""
(?P<option> # very permissive!
(?:(?!{delim})\S)* # non-delimiter non-whitespace
(?:\s+(?:(?!{delim})\S)+)*) # optionally more words
(?P<option>.*?) # very permissive!
\s*(?: # any number of space/tab,
(?P<vi>{delim})\s* # optionally followed by
# any of the allowed

View File

@@ -470,8 +470,6 @@ class CDLL(object):
if name and name.endswith(")") and ".a(" in name:
mode |= _os.RTLD_MEMBER | _os.RTLD_NOW
self._name = name
if handle is not None:
return handle
return _dlopen(name, mode)
def __repr__(self):

26
Lib/ctypes/util.py vendored
View File

@@ -85,10 +85,15 @@ if os.name == "nt":
wintypes.DWORD,
)
# gh-145307: We defer loading psapi.dll until _get_module_handles is called.
# Loading additional DLLs at startup for functionality that may never be
# used is wasteful.
_enum_process_modules = None
_psapi = ctypes.WinDLL('psapi', use_last_error=True)
_enum_process_modules = _psapi["EnumProcessModules"]
_enum_process_modules.restype = wintypes.BOOL
_enum_process_modules.argtypes = (
wintypes.HANDLE,
ctypes.POINTER(wintypes.HMODULE),
wintypes.DWORD,
wintypes.LPDWORD,
)
def _get_module_filename(module: wintypes.HMODULE):
name = (wintypes.WCHAR * 32767)() # UNICODE_STRING_MAX_CHARS
@@ -96,19 +101,8 @@ if os.name == "nt":
return name.value
return None
def _get_module_handles():
global _enum_process_modules
if _enum_process_modules is None:
_psapi = ctypes.WinDLL('psapi', use_last_error=True)
_enum_process_modules = _psapi["EnumProcessModules"]
_enum_process_modules.restype = wintypes.BOOL
_enum_process_modules.argtypes = (
wintypes.HANDLE,
ctypes.POINTER(wintypes.HMODULE),
wintypes.DWORD,
wintypes.LPDWORD,
)
def _get_module_handles():
process = _get_current_process()
space_needed = wintypes.DWORD()
n = 1024

26
Lib/dataclasses.py vendored
View File

@@ -725,10 +725,10 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
annotation_fields=annotation_fields)
def _frozen_set_del_attr(cls, fields, func_builder):
locals = {'__class__': cls,
def _frozen_get_del_attr(cls, fields, func_builder):
locals = {'cls': cls,
'FrozenInstanceError': FrozenInstanceError}
condition = 'type(self) is __class__'
condition = 'type(self) is cls'
if fields:
condition += ' or name in {' + ', '.join(repr(f.name) for f in fields) + '}'
@@ -736,14 +736,14 @@ def _frozen_set_del_attr(cls, fields, func_builder):
('self', 'name', 'value'),
(f' if {condition}:',
' raise FrozenInstanceError(f"cannot assign to field {name!r}")',
f' super(__class__, self).__setattr__(name, value)'),
f' super(cls, self).__setattr__(name, value)'),
locals=locals,
overwrite_error=True)
func_builder.add_fn('__delattr__',
('self', 'name'),
(f' if {condition}:',
' raise FrozenInstanceError(f"cannot delete field {name!r}")',
f' super(__class__, self).__delattr__(name)'),
f' super(cls, self).__delattr__(name)'),
locals=locals,
overwrite_error=True)
@@ -1199,7 +1199,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
overwrite_error='Consider using functools.total_ordering')
if frozen:
_frozen_set_del_attr(cls, field_list, func_builder)
_frozen_get_del_attr(cls, field_list, func_builder)
# Decide if/how we're going to create a hash function.
hash_action = _hash_action[bool(unsafe_hash),
@@ -1292,18 +1292,10 @@ def _update_func_cell_for__class__(f, oldcls, newcls):
# This function doesn't reference __class__, so nothing to do.
return False
# Fix the cell to point to the new class, if it's already pointing
# at the old class.
# at the old class. I'm not convinced that the "is oldcls" test
# is needed, but other than performance can't hurt.
closure = f.__closure__[idx]
try:
contents = closure.cell_contents
except ValueError:
# Cell is empty
return False
# This check makes it so we avoid updating an incorrect cell if the
# class body contains a function that was defined in a different class.
if contents is oldcls:
if closure.cell_contents is oldcls:
closure.cell_contents = newcls
return True
return False

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2007 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

View File

@@ -219,7 +219,7 @@ def encode(string, charset='utf-8', encoding=None, lang=''):
"""
if charset == 'unknown-8bit':
bstring = string.encode('utf-8', 'surrogateescape')
bstring = string.encode('ascii', 'surrogateescape')
else:
bstring = string.encode(charset)
if encoding is None:

View File

@@ -80,8 +80,7 @@ from email import utils
# Useful constants and functions
#
_WSP = ' \t'
WSP = set(_WSP)
WSP = set(' \t')
CFWS_LEADER = WSP | set('(')
SPECIALS = set(r'()<>@,:;.\"[]')
ATOM_ENDS = SPECIALS | WSP
@@ -102,12 +101,6 @@ def make_quoted_pairs(value):
return str(value).replace('\\', '\\\\').replace('"', '\\"')
def make_parenthesis_pairs(value):
"""Escape parenthesis and backslash for use within a comment."""
return str(value).replace('\\', '\\\\') \
.replace('(', '\\(').replace(')', '\\)')
def quote_string(value):
escaped = make_quoted_pairs(value)
return f'"{escaped}"'
@@ -639,11 +632,11 @@ class LocalPart(TokenList):
for tok in self[0] + [DOT]:
if tok.token_type == 'cfws':
continue
if (last_is_tl and tok.token_type == 'dot' and last and
if (last_is_tl and tok.token_type == 'dot' and
last[-1].token_type == 'cfws'):
res[-1] = TokenList(last[:-1])
is_tl = isinstance(tok, TokenList)
if (is_tl and last.token_type == 'dot' and tok and
if (is_tl and last.token_type == 'dot' and
tok[0].token_type == 'cfws'):
res.append(TokenList(tok[1:]))
else:
@@ -881,12 +874,6 @@ class MessageID(MsgID):
class InvalidMessageID(MessageID):
token_type = 'invalid-message-id'
class MessageIDList(TokenList):
token_type = 'message-id-list'
@property
def message_ids(self):
return [x for x in self if x.token_type=='msg-id']
class Header(TokenList):
token_type = 'header'
@@ -946,7 +933,7 @@ class WhiteSpaceTerminal(Terminal):
return ' '
def startswith_fws(self):
return self and self[0] in WSP
return True
class ValueTerminal(Terminal):
@@ -1245,7 +1232,8 @@ def get_bare_quoted_string(value):
bare_quoted_string = BareQuotedString()
value = value[1:]
if value and value[0] == '"':
return bare_quoted_string, value[1:]
token, value = get_qcontent(value)
bare_quoted_string.append(token)
while value and value[0] != '"':
if value[0] in WSP:
token, value = get_fws(value)
@@ -2058,10 +2046,12 @@ def get_address_list(value):
address_list.defects.append(errors.InvalidHeaderDefect(
"invalid address in address-list"))
if value and value[0] != ',':
# Crap after address: add it to the address list
# as an invalid mailbox
# Crap after address; treat it as an invalid mailbox.
# The mailbox info will still be available.
mailbox = address_list[-1][0]
mailbox.token_type = 'invalid-mailbox'
token, value = get_invalid_mailbox(value, ',')
address_list.append(Address([token]))
mailbox.extend(token)
address_list.defects.append(errors.InvalidHeaderDefect(
"invalid address in address-list"))
if value: # Must be a , at this point.
@@ -2181,32 +2171,6 @@ def parse_message_id(value):
return message_id
def parse_message_ids(value):
"""in-reply-to = "In-Reply-To:" 1*msg-id CRLF
references = "References:" 1*msg-id CRLF
"""
message_id_list = MessageIDList()
while value:
if value[0] == ',':
# message id list separated with commas - this is invalid,
# but happens rather frequently in the wild
message_id_list.defects.append(
errors.InvalidHeaderDefect("comma in msg-id list"))
message_id_list.append(
WhiteSpaceTerminal(' ', 'invalid-comma-replacement'))
value = value[1:]
continue
try:
token, value = get_msg_id(value)
message_id_list.append(token)
except errors.HeaderParseError as ex:
token = get_unstructured(value)
message_id_list.append(InvalidMessageID(token))
message_id_list.defects.append(
errors.InvalidHeaderDefect("Invalid msg-id: {!r}".format(ex)))
break
return message_id_list
#
# XXX: As I begin to add additional header parsers, I'm realizing we probably
# have two level of parser routines: the get_XXX methods that get a token in
@@ -2824,12 +2788,8 @@ def _steal_trailing_WSP_if_exists(lines):
if lines and lines[-1] and lines[-1][-1] in WSP:
wsp = lines[-1][-1]
lines[-1] = lines[-1][:-1]
# gh-142006: if the line is now empty, remove it entirely.
if not lines[-1]:
lines.pop()
return wsp
def _refold_parse_tree(parse_tree, *, policy):
"""Return string of contents of parse_tree folded according to RFC rules.
@@ -2838,9 +2798,11 @@ def _refold_parse_tree(parse_tree, *, policy):
maxlen = policy.max_line_length or sys.maxsize
encoding = 'utf-8' if policy.utf8 else 'us-ascii'
lines = [''] # Folded lines to be output
last_word_is_ew = False
last_ew = None # if there is an encoded word in the last line of lines,
# points to the encoded word's first character
leading_whitespace = '' # When we have whitespace between two encoded
# words, we may need to encode the whitespace
# at the beginning of the second word.
last_ew = None # Points to the last encoded character if there's an ew on
# the line
last_charset = None
wrap_as_ew_blocked = 0
want_encoding = False # This is set to True if we need to encode this part
@@ -2875,7 +2837,6 @@ def _refold_parse_tree(parse_tree, *, policy):
if part.token_type == 'mime-parameters':
# Mime parameter folding (using RFC2231) is extra special.
_fold_mime_parameters(part, lines, maxlen, encoding)
last_word_is_ew = False
continue
if want_encoding and not wrap_as_ew_blocked:
@@ -2892,7 +2853,6 @@ def _refold_parse_tree(parse_tree, *, policy):
# XXX what if encoded_part has no leading FWS?
lines.append(newline)
lines[-1] += encoded_part
last_word_is_ew = False
continue
# Either this is not a major syntactic break, so we don't
# want it on a line by itself even if it fits, or it
@@ -2911,16 +2871,11 @@ def _refold_parse_tree(parse_tree, *, policy):
(last_charset == 'unknown-8bit' or
last_charset == 'utf-8' and charset != 'us-ascii')):
last_ew = None
last_ew = _fold_as_ew(
tstr,
lines,
maxlen,
last_ew,
part.ew_combine_allowed,
charset,
last_word_is_ew,
)
last_word_is_ew = True
last_ew = _fold_as_ew(tstr, lines, maxlen, last_ew,
part.ew_combine_allowed, charset, leading_whitespace)
# This whitespace has been added to the lines in _fold_as_ew()
# so clear it now.
leading_whitespace = ''
last_charset = charset
want_encoding = False
continue
@@ -2933,19 +2888,28 @@ def _refold_parse_tree(parse_tree, *, policy):
if len(tstr) <= maxlen - len(lines[-1]):
lines[-1] += tstr
last_word_is_ew = last_word_is_ew and not bool(tstr.strip(_WSP))
continue
# This part is too long to fit. The RFC wants us to break at
# "major syntactic breaks", so unless we don't consider this
# to be one, check if it will fit on the next line by itself.
leading_whitespace = ''
if (part.syntactic_break and
len(tstr) + 1 <= maxlen):
newline = _steal_trailing_WSP_if_exists(lines)
if newline or part.startswith_fws():
# We're going to fold the data onto a new line here. Due to
# the way encoded strings handle continuation lines, we need to
# be prepared to encode any whitespace if the next line turns
# out to start with an encoded word.
lines.append(newline + tstr)
last_word_is_ew = (last_word_is_ew
and not bool(lines[-1].strip(_WSP)))
whitespace_accumulator = []
for char in lines[-1]:
if char not in WSP:
break
whitespace_accumulator.append(char)
leading_whitespace = ''.join(whitespace_accumulator)
last_ew = None
continue
if not hasattr(part, 'encode'):
@@ -2960,13 +2924,6 @@ def _refold_parse_tree(parse_tree, *, policy):
[ValueTerminal(make_quoted_pairs(p), 'ptext')
for p in newparts] +
[ValueTerminal('"', 'ptext')])
if part.token_type == 'comment':
newparts = (
[ValueTerminal('(', 'ptext')] +
[ValueTerminal(make_parenthesis_pairs(p), 'ptext')
if p.token_type == 'ptext' else p
for p in newparts] +
[ValueTerminal(')', 'ptext')])
if not part.as_ew_allowed:
wrap_as_ew_blocked += 1
newparts.append(end_ew_not_allowed)
@@ -2985,11 +2942,10 @@ def _refold_parse_tree(parse_tree, *, policy):
else:
# We can't fold it onto the next line either...
lines[-1] += tstr
last_word_is_ew = last_word_is_ew and not bool(tstr.strip(_WSP))
return policy.linesep.join(lines) + policy.linesep
def _fold_as_ew(to_encode, lines, maxlen, last_ew, ew_combine_allowed, charset, last_word_is_ew):
def _fold_as_ew(to_encode, lines, maxlen, last_ew, ew_combine_allowed, charset, leading_whitespace):
"""Fold string to_encode into lines as encoded word, combining if allowed.
Return the new value for last_ew, or None if ew_combine_allowed is False.
@@ -3004,16 +2960,6 @@ def _fold_as_ew(to_encode, lines, maxlen, last_ew, ew_combine_allowed, charset,
to_encode = str(
get_unstructured(lines[-1][last_ew:] + to_encode))
lines[-1] = lines[-1][:last_ew]
elif last_word_is_ew:
# If we are following up an encoded word with another encoded word,
# any white space between the two will be ignored when decoded.
# Therefore, we encode all to-be-displayed whitespace in the second
# encoded word.
len_without_wsp = len(lines[-1].rstrip(_WSP))
leading_whitespace = lines[-1][len_without_wsp:]
lines[-1] = (lines[-1][:len_without_wsp]
+ (' ' if leading_whitespace else ''))
to_encode = leading_whitespace + to_encode
elif to_encode[0] in WSP:
# We're joining this to non-encoded text, so don't encode
# the leading blank.
@@ -3042,13 +2988,20 @@ def _fold_as_ew(to_encode, lines, maxlen, last_ew, ew_combine_allowed, charset,
while to_encode:
remaining_space = maxlen - len(lines[-1])
text_space = remaining_space - chrome_len
text_space = remaining_space - chrome_len - len(leading_whitespace)
if text_space <= 0:
newline = _steal_trailing_WSP_if_exists(lines)
lines.append(newline or ' ')
new_last_ew = len(lines[-1])
lines.append(' ')
continue
# If we are at the start of a continuation line, prepend whitespace
# (we only want to do this when the line starts with an encoded word
# but if we're folding in this helper function, then we know that we
# are going to be writing out an encoded word.)
if len(lines) > 1 and len(lines[-1]) == 1 and leading_whitespace:
encoded_word = _ew.encode(leading_whitespace, charset=encode_as)
lines[-1] += encoded_word
leading_whitespace = ''
to_encode_word = to_encode[:text_space]
encoded_word = _ew.encode(to_encode_word, charset=encode_as)
excess = len(encoded_word) - remaining_space
@@ -3060,6 +3013,7 @@ def _fold_as_ew(to_encode, lines, maxlen, last_ew, ew_combine_allowed, charset,
excess = len(encoded_word) - remaining_space
lines[-1] += encoded_word
to_encode = to_encode[len(to_encode_word):]
leading_whitespace = ''
if to_encode:
lines.append(' ')

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2002 Python Software Foundation
# Copyright (C) 2002-2007 Python Software Foundation
# Contact: email-sig@python.org
"""Email address parsing code.
@@ -225,7 +225,7 @@ class AddrlistClass:
def __init__(self, field):
"""Initialize a new instance.
'field' is an unparsed address header field, containing
`field' is an unparsed address header field, containing
one or more addresses.
"""
self.specials = '()<>@,:;.\"[]'
@@ -426,14 +426,14 @@ class AddrlistClass:
def getdelimited(self, beginchar, endchars, allowcomments=True):
"""Parse a header fragment delimited by special characters.
'beginchar' is the start character for the fragment.
If self is not looking at an instance of 'beginchar' then
`beginchar' is the start character for the fragment.
If self is not looking at an instance of `beginchar' then
getdelimited returns the empty string.
'endchars' is a sequence of allowable end-delimiting characters.
`endchars' is a sequence of allowable end-delimiting characters.
Parsing stops when one of these is encountered.
If 'allowcomments' is non-zero, embedded RFC 2822 comments are allowed
If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed
within the parsed fragment.
"""
if self.field[self.pos] != beginchar:
@@ -477,7 +477,7 @@ class AddrlistClass:
Optional atomends specifies a different set of end token delimiters
(the default is to use self.atomends). This is used e.g. in
getphraselist() since phrase endings must not include the '.' (which
getphraselist() since phrase endings must not include the `.' (which
is legal in phrases)."""
atomlist = ['']
if atomends is None:

View File

@@ -4,7 +4,6 @@ Allows fine grained feature control of how the package parses and emits data.
"""
import abc
import re
from email import header
from email import charset as _charset
from email.utils import _has_surrogates
@@ -15,14 +14,6 @@ __all__ = [
'compat32',
]
# validation regex from RFC 5322, equivalent to pattern re.compile("[!-9;-~]+$")
valid_header_name_re = re.compile("[\041-\071\073-\176]+$")
def validate_header_name(name):
# Validate header name according to RFC 5322
if not valid_header_name_re.match(name):
raise ValueError(
f"Header field name contains invalid characters: {name!r}")
class _PolicyBase:
@@ -159,7 +150,7 @@ class Policy(_PolicyBase, metaclass=abc.ABCMeta):
wrapping is done. Default is 78.
mangle_from_ -- a flag that, when True escapes From_ lines in the
body of the message by putting a '>' in front of
body of the message by putting a `>' in front of
them. This is used when the message is being
serialized by a generator. Default: False.
@@ -323,7 +314,6 @@ class Compat32(Policy):
"""+
The name and value are returned unmodified.
"""
validate_header_name(name)
return (name, value)
def header_fetch_parse(self, name, value):

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2002 Python Software Foundation
# Copyright (C) 2002-2007 Python Software Foundation
# Author: Ben Gertzfield
# Contact: email-sig@python.org
@@ -15,7 +15,7 @@ This module provides an interface to encode and decode both headers and bodies
with Base64 encoding.
RFC 2045 defines a method for including character set information in an
'encoded-word' in a header. This method is commonly used for 8-bit real names
`encoded-word' in a header. This method is commonly used for 8-bit real names
in To:, From:, Cc:, etc. fields, as well as Subject: lines.
This module does not do the line wrapping or end-of-line character conversion

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2007 Python Software Foundation
# Author: Ben Gertzfield, Barry Warsaw
# Contact: email-sig@python.org
@@ -175,7 +175,7 @@ class Charset:
module expose the following information about a character set:
input_charset: The initial character set specified. Common aliases
are converted to their 'official' email names (e.g. latin_1
are converted to their `official' email names (e.g. latin_1
is converted to iso-8859-1). Defaults to 7-bit us-ascii.
header_encoding: If the character set must be encoded before it can be
@@ -245,7 +245,7 @@ class Charset:
def get_body_encoding(self):
"""Return the content-transfer-encoding used for body encoding.
This is either the string 'quoted-printable' or 'base64' depending on
This is either the string `quoted-printable' or `base64' depending on
the encoding used, or it is a function in which case you should call
the function with a single argument, the Message object being
encoded. The function should then set the Content-Transfer-Encoding

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

2
Lib/email/errors.py vendored
View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2004 Python Software Foundation
# Copyright (C) 2004-2006 Python Software Foundation
# Authors: Baxter, Wouters and Warsaw
# Contact: email-sig@python.org
@@ -30,7 +30,7 @@ from io import StringIO
NLCRE = re.compile(r'\r\n|\r|\n')
NLCRE_bol = re.compile(r'(\r\n|\r|\n)')
NLCRE_eol = re.compile(r'(\r\n|\r|\n)\z')
NLCRE_eol = re.compile(r'(\r\n|\r|\n)\Z')
NLCRE_crack = re.compile(r'(\r\n|\r|\n)')
# RFC 5322 section 3.6.8 Optional fields. ftext is %d33-57 / %d59-126, Any character
# except controls, SP, and ":".
@@ -504,9 +504,10 @@ class FeedParser:
self._input.unreadline(line)
return
else:
# Weirdly placed unix-from line.
# Weirdly placed unix-from line. Note this as a defect
# and ignore it.
defect = errors.MisplacedEnvelopeHeaderDefect(line)
self.policy.handle_defect(self._cur, defect)
self._cur.defects.append(defect)
continue
# Split the line on the colon separating field name from value.
# There will always be a colon, because if there wasn't the part of
@@ -518,7 +519,7 @@ class FeedParser:
# message. Track the error but keep going.
if i == 0:
defect = errors.InvalidHeaderDefect("Missing header name.")
self.policy.handle_defect(self._cur, defect)
self._cur.defects.append(defect)
continue
assert i>0, "_parse_headers fed line with no : and no leading WS"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2010 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org
@@ -22,7 +22,6 @@ NL = '\n' # XXX: no longer used by the code below.
NLCRE = re.compile(r'\r\n|\r|\n')
fcre = re.compile(r'^From ', re.MULTILINE)
NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
class Generator:
@@ -44,7 +43,7 @@ class Generator:
Optional mangle_from_ is a flag that, when True (the default if policy
is not set), escapes From_ lines in the body of the message by putting
a '>' in front of them.
a `>' in front of them.
Optional maxheaderlen specifies the longest length for a non-continued
header. When a header line is longer (in characters, with tabs
@@ -77,7 +76,7 @@ class Generator:
unixfrom is a flag that forces the printing of a Unix From_ delimiter
before the first object in the message tree. If the original message
has no From_ delimiter, a 'standard' one is crafted. By default, this
has no From_ delimiter, a `standard' one is crafted. By default, this
is False to inhibit the printing of any From_ delimiter.
Note that for subobjects, no From_ line is printed.
@@ -228,7 +227,7 @@ class Generator:
folded = self.policy.fold(h, v)
if self.policy.verify_generated_headers:
linesep = self.policy.linesep
if not folded.endswith(linesep):
if not folded.endswith(self.policy.linesep):
raise HeaderWriteError(
f'folded header does not end with {linesep!r}: {folded!r}')
if NEWLINE_WITHOUT_FWSP.search(folded.removesuffix(linesep)):
@@ -392,7 +391,7 @@ class Generator:
b = boundary
counter = 0
while True:
cre = cls._compile_re('^--' + re.escape(b) + '(--)?\r?$', re.MULTILINE)
cre = cls._compile_re('^--' + re.escape(b) + '(--)?$', re.MULTILINE)
if not cre.search(text):
break
b = boundary + '.' + str(counter)
@@ -430,16 +429,7 @@ class BytesGenerator(Generator):
# This is almost the same as the string version, except for handling
# strings with 8bit bytes.
for h, v in msg.raw_items():
folded = self.policy.fold_binary(h, v)
if self.policy.verify_generated_headers:
linesep = self.policy.linesep.encode()
if not folded.endswith(linesep):
raise HeaderWriteError(
f'folded header does not end with {linesep!r}: {folded!r}')
if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)):
raise HeaderWriteError(
f'folded header contains newline: {folded!r}')
self._fp.write(folded)
self._fp.write(self.policy.fold_binary(h, v))
# A blank line always separates headers from body
self.write(self._NL)
@@ -477,7 +467,7 @@ class DecodedGenerator(Generator):
argument is allowed.
Walks through all subparts of a message. If the subpart is of main
type 'text', then it prints the decoded payload of the subpart.
type `text', then it prints the decoded payload of the subpart.
Otherwise, fmt is a format string that is used instead of the message
payload. fmt is expanded with the following keywords (in

8
Lib/email/header.py vendored
View File

@@ -1,4 +1,4 @@
# Copyright (C) 2002 Python Software Foundation
# Copyright (C) 2002-2007 Python Software Foundation
# Author: Ben Gertzfield, Barry Warsaw
# Contact: email-sig@python.org
@@ -201,7 +201,7 @@ class Header:
The maximum line length can be specified explicitly via maxlinelen. For
splitting the first line to a shorter value (to account for the field
header which isn't included in s, e.g. 'Subject') pass in the name of
header which isn't included in s, e.g. `Subject') pass in the name of
the field in header_name. The default maxlinelen is 78 as recommended
by RFC 2822.
@@ -285,7 +285,7 @@ class Header:
output codec of the charset. If the string cannot be encoded to the
output codec, a UnicodeError will be raised.
Optional 'errors' is passed as the errors argument to the decode
Optional `errors' is passed as the errors argument to the decode
call if s is a byte string.
"""
if charset is None:
@@ -335,7 +335,7 @@ class Header:
Optional splitchars is a string containing characters which should be
given extra weight by the splitting algorithm during normal header
wrapping. This is in very rough support of RFC 2822's 'higher level
wrapping. This is in very rough support of RFC 2822's `higher level
syntactic breaks': split points preceded by a splitchar are preferred
during line splitting, with the characters preferred in the order in
which they appear in the string. Space and tab may be included in the

View File

@@ -534,18 +534,6 @@ class MessageIDHeader:
kwds['defects'].extend(parse_tree.all_defects)
class ReferencesHeader:
max_count = 1
value_parser = staticmethod(parser.parse_message_ids)
@classmethod
def parse(cls, value, kwds):
kwds['parse_tree'] = parse_tree = cls.value_parser(value)
kwds['decoded'] = str(parse_tree)
kwds['defects'].extend(parse_tree.all_defects)
# The header factory #
_default_header_map = {
@@ -569,8 +557,6 @@ _default_header_map = {
'content-disposition': ContentDispositionHeader,
'content-transfer-encoding': ContentTransferEncodingHeader,
'message-id': MessageIDHeader,
'in-reply-to': ReferencesHeader,
'references': ReferencesHeader,
}
class HeaderRegistry:

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org
@@ -43,8 +43,8 @@ def body_line_iterator(msg, decode=False):
def typed_subpart_iterator(msg, maintype='text', subtype=None):
"""Iterate over the subparts with a given MIME type.
Use 'maintype' as the main MIME type to match against; this defaults to
"text". Optional 'subtype' is the MIME subtype to match against; if
Use `maintype' as the main MIME type to match against; this defaults to
"text". Optional `subtype' is the MIME subtype to match against; if
omitted, only the main type is matched.
"""
for subpart in msg.walk():

28
Lib/email/message.py vendored
View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2007 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org
@@ -21,7 +21,7 @@ Charset = _charset.Charset
SEMISPACE = '; '
# Regular expression that matches 'special' characters in parameters, the
# Regular expression that matches `special' characters in parameters, the
# existence of which force quoting of the parameter value.
tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
@@ -147,7 +147,7 @@ class Message:
multipart or a message/rfc822), then the payload is a list of Message
objects, otherwise it is a string.
Message objects implement part of the 'mapping' interface, which assumes
Message objects implement part of the `mapping' interface, which assumes
there is exactly one occurrence of the header per message. Some headers
do in fact appear multiple times (e.g. Received) and for those headers,
you must use the explicit API to set or get all the headers. Not all of
@@ -609,7 +609,7 @@ class Message:
"""Return the message's content type.
The returned string is coerced to lower case of the form
'maintype/subtype'. If there was no Content-Type header in the
`maintype/subtype'. If there was no Content-Type header in the
message, the default type as given by get_default_type() will be
returned. Since according to RFC 2045, messages always have a default
type this will always return a value.
@@ -632,7 +632,7 @@ class Message:
def get_content_maintype(self):
"""Return the message's main content type.
This is the 'maintype' part of the string returned by
This is the `maintype' part of the string returned by
get_content_type().
"""
ctype = self.get_content_type()
@@ -641,14 +641,14 @@ class Message:
def get_content_subtype(self):
"""Returns the message's sub-content type.
This is the 'subtype' part of the string returned by
This is the `subtype' part of the string returned by
get_content_type().
"""
ctype = self.get_content_type()
return ctype.split('/')[1]
def get_default_type(self):
"""Return the 'default' content type.
"""Return the `default' content type.
Most messages have a default content type of text/plain, except for
messages that are subparts of multipart/digest containers. Such
@@ -657,7 +657,7 @@ class Message:
return self._default_type
def set_default_type(self, ctype):
"""Set the 'default' content type.
"""Set the `default' content type.
ctype should be either "text/plain" or "message/rfc822", although this
is not enforced. The default content type is not stored in the
@@ -690,8 +690,8 @@ class Message:
"""Return the message's Content-Type parameters, as a list.
The elements of the returned list are 2-tuples of key/value pairs, as
split on the '=' sign. The left hand side of the '=' is the key,
while the right hand side is the value. If there is no '=' sign in
split on the `=' sign. The left hand side of the `=' is the key,
while the right hand side is the value. If there is no `=' sign in
the parameter the value is the empty string. The value is as
described in the get_param() method.
@@ -851,9 +851,9 @@ class Message:
"""Return the filename associated with the payload if present.
The filename is extracted from the Content-Disposition header's
'filename' parameter, and it is unquoted. If that header is missing
the 'filename' parameter, this method falls back to looking for the
'name' parameter.
`filename' parameter, and it is unquoted. If that header is missing
the `filename' parameter, this method falls back to looking for the
`name' parameter.
"""
missing = object()
filename = self.get_param('filename', missing, 'content-disposition')
@@ -866,7 +866,7 @@ class Message:
def get_boundary(self, failobj=None):
"""Return the boundary associated with the payload if present.
The boundary is extracted from the Content-Type header's 'boundary'
The boundary is extracted from the Content-Type header's `boundary'
parameter, and it is unquoted.
"""
missing = object()

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Keith Dart
# Contact: email-sig@python.org

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2007 Python Software Foundation
# Author: Anthony Baxter
# Contact: email-sig@python.org

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2002 Python Software Foundation
# Copyright (C) 2002-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org
@@ -21,7 +21,7 @@ class MIMEMultipart(MIMEBase):
Content-Type and MIME-Version headers.
_subtype is the subtype of the multipart content type, defaulting to
'mixed'.
`mixed'.
boundary is the multipart boundary string. By default it is
calculated as needed.

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2002 Python Software Foundation
# Copyright (C) 2002-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org

2
Lib/email/parser.py vendored
View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2007 Python Software Foundation
# Author: Barry Warsaw, Thomas Wouters, Anthony Baxter
# Contact: email-sig@python.org

9
Lib/email/policy.py vendored
View File

@@ -4,13 +4,7 @@ code that adds all the email6 features.
import re
import sys
from email._policybase import (
Compat32,
Policy,
_extend_docstrings,
compat32,
validate_header_name
)
from email._policybase import Policy, Compat32, compat32, _extend_docstrings
from email.utils import _has_surrogates
from email.headerregistry import HeaderRegistry as HeaderRegistry
from email.contentmanager import raw_data_manager
@@ -144,7 +138,6 @@ class EmailPolicy(Policy):
CR or LF characters.
"""
validate_header_name(name)
if hasattr(value, 'name') and value.name.lower() == name.lower():
return (name, value)
if isinstance(value, str) and len(value.splitlines())>1:

View File

@@ -1,11 +1,11 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2006 Python Software Foundation
# Author: Ben Gertzfield
# Contact: email-sig@python.org
"""Quoted-printable content transfer encoding per RFCs 2045-2047.
This module handles the content transfer encoding method defined in RFC 2045
to encode US ASCII-like 8-bit data called 'quoted-printable'. It is used to
to encode US ASCII-like 8-bit data called `quoted-printable'. It is used to
safely encode text that is in a character set similar to the 7-bit US ASCII
character set, but that includes some 8-bit characters that are normally not
allowed in email bodies or headers.
@@ -17,7 +17,7 @@ This module provides an interface to encode and decode both headers and bodies
with quoted-printable encoding.
RFC 2045 defines a method for including character set information in an
'encoded-word' in a header. This method is commonly used for 8-bit real names
`encoded-word' in a header. This method is commonly used for 8-bit real names
in To:/From:/Cc: etc. fields, as well as Subject: lines.
This module does not do the line wrapping or end-of-line character
@@ -127,7 +127,7 @@ def quote(c):
def header_encode(header_bytes, charset='iso-8859-1'):
"""Encode a single header line with quoted-printable (like) encoding.
Defined in RFC 2045, this 'Q' encoding is similar to quoted-printable, but
Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but
used specifically for email header fields to allow charsets with mostly 7
bit characters (and some 8 bit) to remain more or less readable in non-RFC
2045 aware mail clients.
@@ -272,7 +272,7 @@ def decode(encoded, eol=NL):
decoded += eol
# Special case if original string did not end with eol
if encoded[-1] not in '\r\n' and decoded.endswith(eol):
decoded = decoded[:-len(eol)]
decoded = decoded[:-1]
return decoded
@@ -290,7 +290,7 @@ def _unquote_match(match):
# Header decoding is done a bit differently
def header_decode(s):
"""Decode a string encoded with RFC 2045 MIME header 'Q' encoding.
"""Decode a string encoded with RFC 2045 MIME header `Q' encoding.
This function does not parse a full MIME header value encoded with
quoted-printable (like =?iso-8859-1?q?Hello_World?=) -- please use

12
Lib/email/utils.py vendored
View File

@@ -1,4 +1,4 @@
# Copyright (C) 2001 Python Software Foundation
# Copyright (C) 2001-2010 Python Software Foundation
# Author: Barry Warsaw
# Contact: email-sig@python.org
@@ -472,15 +472,23 @@ def collapse_rfc2231_value(value, errors='replace',
# better than not having it.
#
def localtime(dt=None):
def localtime(dt=None, isdst=None):
"""Return local time as an aware datetime object.
If called without arguments, return current time. Otherwise *dt*
argument should be a datetime instance, and it is converted to the
local time zone according to the system time zone database. If *dt* is
naive (that is, dt.tzinfo is None), it is assumed to be in local time.
The isdst parameter is ignored.
"""
if isdst is not None:
import warnings
warnings._deprecated(
"The 'isdst' parameter to 'localtime'",
message='{name} is deprecated and slated for removal in Python {remove}',
remove=(3, 14),
)
if dt is None:
dt = datetime.datetime.now()
return dt.astimezone()

View File

@@ -33,7 +33,6 @@ import sys
from . import aliases
_cache = {}
_MAXCACHE = 500
_unknown = '--unknown--'
_import_tail = ['*']
_aliases = aliases.aliases
@@ -116,8 +115,6 @@ def search_function(encoding):
if mod is None:
# Cache misses
if len(_cache) >= _MAXCACHE:
_cache.clear()
_cache[encoding] = None
return None
@@ -139,8 +136,6 @@ def search_function(encoding):
entry = codecs.CodecInfo(*entry)
# Cache the codec registry entry
if len(_cache) >= _MAXCACHE:
_cache.clear()
_cache[encoding] = entry
# Register its aliases (without overwriting previously registered

View File

@@ -10,14 +10,13 @@ from shutil import copy2
__all__ = ["version", "bootstrap"]
_PIP_VERSION = "26.1.1"
_PIP_VERSION = "25.3"
# Directory of system wheel packages. Some Linux distribution packaging
# policies recommend against bundling dependencies. For example, Fedora
# installs wheel packages in the /usr/share/python-wheels/ directory and don't
# install the ensurepip._bundled package.
_pkg_dir = sysconfig.get_config_var('WHEEL_PKG_DIR')
if _pkg_dir:
if (_pkg_dir := sysconfig.get_config_var('WHEEL_PKG_DIR')) is not None:
_WHEEL_PKG_DIR = Path(_pkg_dir).resolve()
else:
_WHEEL_PKG_DIR = None

27
Lib/glob.py vendored
View File

@@ -15,7 +15,7 @@ __all__ = ["glob", "iglob", "escape", "translate"]
def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
include_hidden=False):
"""Return a list of paths matching a `pathname` pattern.
"""Return a list of paths matching a pathname pattern.
The pattern may contain simple shell-style wildcards a la
fnmatch. Unlike fnmatch, filenames starting with a
@@ -25,15 +25,6 @@ def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
The order of the returned list is undefined. Sort it if you need a
particular order.
If `root_dir` is not None, it should be a path-like object specifying the
root directory for searching. It has the same effect as changing the
current directory before calling it (without actually
changing it). If pathname is relative, the result will contain
paths relative to `root_dir`.
If `dir_fd` is not None, it should be a file descriptor referring to a
directory, and paths will then be relative to that directory.
If `include_hidden` is true, the patterns '*', '?', '**' will match hidden
directories.
@@ -45,7 +36,7 @@ def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
include_hidden=False):
"""Return an iterator which yields the paths matching a `pathname` pattern.
"""Return an iterator which yields the paths matching a pathname pattern.
The pattern may contain simple shell-style wildcards a la
fnmatch. However, unlike fnmatch, filenames starting with a
@@ -55,19 +46,7 @@ def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
The order of the returned paths is undefined. Sort them if you need a
particular order.
If `root_dir` is not None, it should be a path-like object specifying
the root directory for searching. It has the same effect as changing
the current directory before calling it (without actually
changing it). If pathname is relative, the result will contain
paths relative to `root_dir`.
If `dir_fd` is not None, it should be a file descriptor referring to a
directory, and paths will then be relative to that directory.
If `include_hidden` is true, the patterns '*', '?', '**' will match hidden
directories.
If `recursive` is true, the pattern '**' will match any files and
If recursive is true, the pattern '**' will match any files and
zero or more directories and subdirectories.
"""
sys.audit("glob.glob", pathname, recursive)

11
Lib/http/client.py vendored
View File

@@ -972,22 +972,13 @@ class HTTPConnection:
return ip
def _tunnel(self):
if _contains_disallowed_url_pchar_re.search(self._tunnel_host):
raise ValueError('Tunnel host can\'t contain control characters %r'
% (self._tunnel_host,))
connect = b"CONNECT %s:%d %s\r\n" % (
self._wrap_ipv6(self._tunnel_host.encode("idna")),
self._tunnel_port,
self._http_vsn_str.encode("ascii"))
headers = [connect]
for header, value in self._tunnel_headers.items():
header_bytes = header.encode("latin-1")
value_bytes = value.encode("latin-1")
if not _is_legal_header_name(header_bytes):
raise ValueError('Invalid header name %r' % (header_bytes,))
if _is_illegal_header_value(value_bytes):
raise ValueError('Invalid header value %r' % (value_bytes,))
headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes))
headers.append(f"{header}: {value}\r\n".encode("latin-1"))
headers.append(b"\r\n")
# Making a single send() call instead of one per line encourages
# the host OS to use a more optimal packet size instead of

30
Lib/http/cookies.py vendored
View File

@@ -337,16 +337,9 @@ class Morsel(dict):
key = key.lower()
if key not in self._reserved:
raise CookieError("Invalid attribute %r" % (key,))
if _has_control_character(key, val):
raise CookieError("Control characters are not allowed in "
f"cookies {key!r} {val!r}")
data[key] = val
dict.update(self, data)
def __ior__(self, values):
self.update(values)
return self
def isReservedKey(self, K):
return K.lower() in self._reserved
@@ -372,15 +365,9 @@ class Morsel(dict):
}
def __setstate__(self, state):
key = state['key']
value = state['value']
coded_value = state['coded_value']
if _has_control_character(key, value, coded_value):
raise CookieError("Control characters are not allowed in cookies "
f"{key!r} {value!r} {coded_value!r}")
self._key = key
self._value = value
self._coded_value = coded_value
self._key = state['key']
self._value = state['value']
self._coded_value = state['coded_value']
def output(self, attrs=None, header="Set-Cookie:"):
return "%s %s" % (header, self.OutputString(attrs))
@@ -391,21 +378,14 @@ class Morsel(dict):
return '<%s: %s>' % (self.__class__.__name__, self.OutputString())
def js_output(self, attrs=None):
import base64
# Print javascript
output_string = self.OutputString(attrs)
if _has_control_character(output_string):
raise CookieError("Control characters are not allowed in cookies")
# Base64-encode value to avoid template
# injection in cookie values.
output_encoded = base64.b64encode(output_string.encode('utf-8')).decode("ascii")
return """
<script type="text/javascript">
<!-- begin hiding
document.cookie = atob(\"%s\");
document.cookie = \"%s\";
// end hiding -->
</script>
""" % (output_encoded,)
""" % (self.OutputString(attrs).replace('"', r'\"'))
def OutputString(self, attrs=None):
# Build up our result

5
Lib/inspect.py vendored
View File

@@ -1,7 +1,7 @@
"""Get useful information from live Python objects.
This module encapsulates the interface provided by the internal special
attributes (co_*, tb_*, etc.) in a friendlier fashion.
attributes (co_*, im_*, tb_*, etc.) in a friendlier fashion.
It also provides some help for examining source code and class layout.
Here are some of the useful functions provided by this module:
@@ -2660,12 +2660,11 @@ class Parameter:
The annotation for the parameter if specified. If the
parameter has no annotation, this attribute is set to
`Parameter.empty`.
* kind
* kind : str
Describes how argument values are bound to the parameter.
Possible values: `Parameter.POSITIONAL_ONLY`,
`Parameter.POSITIONAL_OR_KEYWORD`, `Parameter.VAR_POSITIONAL`,
`Parameter.KEYWORD_ONLY`, `Parameter.VAR_KEYWORD`.
Every value has a `description` attribute describing meaning.
"""
__slots__ = ('_name', '_kind', '_default', '_annotation')

View File

@@ -1475,6 +1475,8 @@ class Logger(Filterer):
level, and "input.csv", "input.xls" and "input.gnu" for the sub-levels.
There is no arbitrary limit to the depth of nesting.
"""
_tls = threading.local()
def __init__(self, name, level=NOTSET):
"""
Initialize the logger with a name and an optional level.
@@ -1671,14 +1673,19 @@ class Logger(Filterer):
This method is used for unpickled records received from a socket, as
well as those created locally. Logger-level filtering is applied.
"""
if self.disabled:
if self._is_disabled():
return
maybe_record = self.filter(record)
if not maybe_record:
return
if isinstance(maybe_record, LogRecord):
record = maybe_record
self.callHandlers(record)
self._tls.in_progress = True
try:
maybe_record = self.filter(record)
if not maybe_record:
return
if isinstance(maybe_record, LogRecord):
record = maybe_record
self.callHandlers(record)
finally:
self._tls.in_progress = False
def addHandler(self, hdlr):
"""
@@ -1766,7 +1773,7 @@ class Logger(Filterer):
"""
Is this logger enabled for level 'level'?
"""
if self.disabled:
if self._is_disabled():
return False
try:
@@ -1816,6 +1823,11 @@ class Logger(Filterer):
if isinstance(item, Logger) and item.parent is self and
_hierlevel(item) == 1 + _hierlevel(item.parent))
def _is_disabled(self):
# We need to use getattr as it will only be set the first time a log
# message is recorded on any given thread
return self.disabled or getattr(self._tls, 'in_progress', False)
def __repr__(self):
level = getLevelName(self.getEffectiveLevel())
return '<%s %s (%s)>' % (self.__class__.__name__, self.name, level)
@@ -1852,9 +1864,9 @@ class LoggerAdapter(object):
def __init__(self, logger, extra=None, merge_extra=False):
"""
Initialize the adapter with a logger and an optional dict-like object
which provides contextual information. This constructor signature
allows easy stacking of LoggerAdapters, if so desired.
Initialize the adapter with a logger and a dict-like object which
provides contextual information. This constructor signature allows
easy stacking of LoggerAdapters, if so desired.
You can effectively pass keyword arguments as shown in the
following example:
@@ -1885,9 +1897,8 @@ class LoggerAdapter(object):
Normally, you'll only need to override this one method in a
LoggerAdapter subclass for your specific needs.
"""
if self.merge_extra and kwargs.get("extra") is not None:
if self.extra is not None:
kwargs["extra"] = {**self.extra, **kwargs["extra"]}
if self.merge_extra and "extra" in kwargs:
kwargs["extra"] = {**self.extra, **kwargs["extra"]}
else:
kwargs["extra"] = self.extra
return msg, kwargs

14
Lib/logging/config.py vendored
View File

@@ -865,8 +865,6 @@ class DictConfigurator(BaseConfigurator):
else:
factory = klass
kwargs = {k: config[k] for k in config if (k != '.' and valid_ident(k))}
# When deprecation ends for using the 'strm' parameter, remove the
# "except TypeError ..."
try:
result = factory(**kwargs)
except TypeError as te:
@@ -878,15 +876,6 @@ class DictConfigurator(BaseConfigurator):
#(e.g. by Django)
kwargs['strm'] = kwargs.pop('stream')
result = factory(**kwargs)
import warnings
warnings.warn(
"Support for custom logging handlers with the 'strm' argument "
"is deprecated and scheduled for removal in Python 3.16. "
"Define handlers with the 'stream' argument instead.",
DeprecationWarning,
stacklevel=2,
)
if formatter:
result.setFormatter(formatter)
if level is not None:
@@ -1017,8 +1006,7 @@ def listen(port=DEFAULT_LOGGING_CONFIG_PORT, verify=None):
A simple TCP socket-based logging config receiver.
"""
allow_reuse_address = True
allow_reuse_port = False
allow_reuse_address = 1
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
handler=None, ready=None, verify=None):

View File

@@ -196,11 +196,7 @@ class RotatingFileHandler(BaseRotatingHandler):
if self.stream is None: # delay was set...
self.stream = self._open()
if self.maxBytes > 0: # are we rolling over?
try:
pos = self.stream.tell()
except io.UnsupportedOperation:
# gh-143237: Never rollover a named pipe.
return False
pos = self.stream.tell()
if not pos:
# gh-116263: Never rollover an empty file
return False
@@ -859,7 +855,7 @@ class SysLogHandler(logging.Handler):
}
def __init__(self, address=('localhost', SYSLOG_UDP_PORT),
facility=LOG_USER, socktype=None, timeout=None):
facility=LOG_USER, socktype=None):
"""
Initialize a handler.
@@ -876,7 +872,6 @@ class SysLogHandler(logging.Handler):
self.address = address
self.facility = facility
self.socktype = socktype
self.timeout = timeout
self.socket = None
self.createSocket()
@@ -938,8 +933,6 @@ class SysLogHandler(logging.Handler):
err = sock = None
try:
sock = socket.socket(af, socktype, proto)
if self.timeout:
sock.settimeout(self.timeout)
if socktype == socket.SOCK_STREAM:
sock.connect(sa)
break
@@ -1536,19 +1529,6 @@ class QueueListener(object):
self._thread = None
self.respect_handler_level = respect_handler_level
def __enter__(self):
"""
For use as a context manager. Starts the listener.
"""
self.start()
return self
def __exit__(self, *args):
"""
For use as a context manager. Stops the listener.
"""
self.stop()
def dequeue(self, block):
"""
Dequeue a record and return it, optionally blocking.

12
Lib/pickle.py vendored
View File

@@ -904,11 +904,17 @@ class _Pickler:
# Write data in-band
# XXX The C implementation avoids a copy here
buf = m.tobytes()
in_memo = id(buf) in self.memo
if m.readonly:
self._save_bytes_no_memo(buf)
if in_memo:
self._save_bytes_no_memo(buf)
else:
self.save_bytes(buf)
else:
self._save_bytearray_no_memo(buf)
self.memoize(obj)
if in_memo:
self._save_bytearray_no_memo(buf)
else:
self.save_bytearray(buf)
else:
# Write data out-of-band
self.write(NEXT_BUFFER)

117
Lib/platform.py vendored Normal file → Executable file
View File

@@ -1,3 +1,5 @@
#!/usr/bin/env python3
""" This module tries to retrieve as much platform-identifying data as
possible. It makes this information available via function APIs.
@@ -31,7 +33,6 @@
#
# <see CVS and SVN checkin messages for history>
#
# 1.0.9 - added invalidate_caches() function to invalidate cached values
# 1.0.8 - changed Windows support to read version from kernel32.dll
# 1.0.7 - added DEV_NULL
# 1.0.6 - added linux_distribution()
@@ -110,7 +111,7 @@ __copyright__ = """
"""
__version__ = '1.0.9'
__version__ = '1.0.8'
import collections
import os
@@ -173,11 +174,6 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
"""
if not executable:
if sys.platform == "emscripten":
# Emscripten's os.confstr reports that it is glibc, so special case
# it.
ver = ".".join(str(x) for x in sys._emscripten_info.emscripten_version)
return ("emscripten", ver)
try:
ver = os.confstr('CS_GNU_LIBC_VERSION')
# parse 'glibc 2.28' as ('glibc', '2.28')
@@ -194,26 +190,22 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
# sys.executable is not set.
return lib, version
libc_search = re.compile(br"""
(__libc_init)
| (GLIBC_([0-9.]+))
| (libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)
| (musl-([0-9.]+))
| ((?:libc\.|ld-)musl(?:-\w+)?.so(?:\.(\d[0-9.]*))?)
""",
re.ASCII | re.VERBOSE)
libc_search = re.compile(b'(__libc_init)'
b'|'
b'(GLIBC_([0-9.]+))'
b'|'
br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
V = _comparable_version
# We use os.path.realpath()
# here to work around problems with Cygwin not being
# able to open symlinks for reading
executable = os.path.realpath(executable)
ver = None
with open(executable, 'rb') as f:
binary = f.read(chunksize)
pos = 0
while pos < len(binary):
if b'libc' in binary or b'GLIBC' in binary or b'musl' in binary:
if b'libc' in binary or b'GLIBC' in binary:
m = libc_search.search(binary, pos)
else:
m = None
@@ -225,35 +217,26 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
continue
if not m:
break
decoded_groups = [s.decode('latin1') if s is not None else s
for s in m.groups()]
(libcinit, glibc, glibcversion, so, threads, soversion,
musl, muslversion, musl_so, musl_sover) = decoded_groups
libcinit, glibc, glibcversion, so, threads, soversion = [
s.decode('latin1') if s is not None else s
for s in m.groups()]
if libcinit and not lib:
lib = 'libc'
elif glibc:
if lib != 'glibc':
lib = 'glibc'
ver = glibcversion
elif V(glibcversion) > V(ver):
ver = glibcversion
version = glibcversion
elif V(glibcversion) > V(version):
version = glibcversion
elif so:
if lib not in ('glibc', 'musl'):
if lib != 'glibc':
lib = 'libc'
if soversion and (not ver or V(soversion) > V(ver)):
ver = soversion
if threads and ver[-len(threads):] != threads:
ver = ver + threads
elif musl:
lib = 'musl'
if not ver or V(muslversion) > V(ver):
ver = muslversion
elif musl_so:
lib = 'musl'
if musl_sover and (not ver or V(musl_sover) > V(ver)):
ver = musl_sover
if soversion and (not version or V(soversion) > V(version)):
version = soversion
if threads and version[-len(threads):] != threads:
version = version + threads
pos = m.end()
return lib, version if ver is None else ver
return lib, version
def _norm_version(version, build=''):
@@ -566,7 +549,7 @@ def java_ver(release='', vendor='', vminfo=('', '', ''), osinfo=('', '', '')):
warnings._deprecated('java_ver', remove=(3, 15))
# Import the needed APIs
try:
import java.lang # noqa: F401
import java.lang
except ImportError:
return release, vendor, vminfo, osinfo
@@ -1209,7 +1192,7 @@ def _sys_version(sys_version=None):
# CPython
cpython_sys_version_parser = re.compile(
r'([\w.+]+)\s*' # "version<space>"
r'(?:free-threading build\s+)?' # "free-threading-build<space>"
r'(?:experimental free-threading build\s+)?' # "free-threading-build<space>"
r'\(#?([^,]+)' # "(#buildno"
r'(?:,\s*([\w ]*)' # ", builddate"
r'(?:,\s*([\w :]*))?)?\)\s*' # ", buildtime)<space>"
@@ -1466,55 +1449,11 @@ def freedesktop_os_release():
return _os_release_cache.copy()
def invalidate_caches():
"""Invalidate the cached results."""
global _uname_cache
_uname_cache = None
global _os_release_cache
_os_release_cache = None
_sys_version_cache.clear()
_platform_cache.clear()
### Command line interface
def _parse_args(args: list[str] | None):
import argparse
parser = argparse.ArgumentParser(color=True)
parser.add_argument("args", nargs="*", choices=["nonaliased", "terse"])
parser.add_argument(
"--terse",
action="store_true",
help=(
"return only the absolute minimum information needed "
"to identify the platform"
),
)
parser.add_argument(
"--nonaliased",
dest="aliased",
action="store_false",
help=(
"disable system/OS name aliasing. If aliasing is enabled, "
"some platforms report system names different from "
"their common names, e.g. SunOS is reported as Solaris"
),
)
return parser.parse_args(args)
def _main(args: list[str] | None = None):
args = _parse_args(args)
terse = args.terse or ("terse" in args.args)
aliased = args.aliased and ('nonaliased' not in args.args)
if __name__ == '__main__':
# Default is to print the aliased verbose platform string
terse = ('terse' in sys.argv or '--terse' in sys.argv)
aliased = (not 'nonaliased' in sys.argv and not '--nonaliased' in sys.argv)
print(platform(aliased, terse))
if __name__ == "__main__":
_main()
sys.exit(0)

6
Lib/plistlib.py vendored
View File

@@ -21,7 +21,7 @@ datetime.datetime objects.
Generate Plist example:
import datetime as dt
import datetime
import plistlib
pl = dict(
@@ -37,7 +37,7 @@ Generate Plist example:
),
someData = b"<binary gunk>",
someMoreData = b"<lots of binary gunk>" * 10,
aDate = dt.datetime.now()
aDate = datetime.datetime.now()
)
print(plistlib.dumps(pl).decode())
@@ -384,7 +384,7 @@ class _PlistWriter(_DumbXMLWriter):
self._indent_level -= 1
maxlinelength = max(
16,
76 - len((self.indent * self._indent_level).expandtabs()))
76 - len(self.indent.replace(b"\t", b" " * 8) * self._indent_level))
for line in _encode_base64(data, maxlinelength).split(b"\n"):
if line:

615
Lib/profile.py vendored
View File

@@ -1,615 +0,0 @@
#
# Class for profiling python code. rev 1.0 6/2/94
#
# Written by James Roskind
# Based on prior profile module by Sjoerd Mullender...
# which was hacked somewhat by: Guido van Rossum
"""Class for profiling Python code."""
# Copyright Disney Enterprises, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
import importlib.machinery
import io
import sys
import time
import marshal
__all__ = ["run", "runctx", "Profile"]
# Sample timer for use with
#i_count = 0
#def integer_timer():
# global i_count
# i_count = i_count + 1
# return i_count
#itimes = integer_timer # replace with C coded timer returning integers
class _Utils:
"""Support class for utility functions which are shared by
profile.py and cProfile.py modules.
Not supposed to be used directly.
"""
def __init__(self, profiler):
self.profiler = profiler
def run(self, statement, filename, sort):
prof = self.profiler()
try:
prof.run(statement)
except SystemExit:
pass
finally:
self._show(prof, filename, sort)
def runctx(self, statement, globals, locals, filename, sort):
prof = self.profiler()
try:
prof.runctx(statement, globals, locals)
except SystemExit:
pass
finally:
self._show(prof, filename, sort)
def _show(self, prof, filename, sort):
if filename is not None:
prof.dump_stats(filename)
else:
prof.print_stats(sort)
#**************************************************************************
# The following are the static member functions for the profiler class
# Note that an instance of Profile() is *not* needed to call them.
#**************************************************************************
def run(statement, filename=None, sort=-1):
"""Run statement under profiler optionally saving results in filename
This function takes a single argument that can be passed to the
"exec" statement, and an optional file name. In all cases this
routine attempts to "exec" its first argument and gather profiling
statistics from the execution. If no file name is present, then this
function automatically prints a simple profiling report, sorted by the
standard name string (file/line/function-name) that is presented in
each line.
"""
return _Utils(Profile).run(statement, filename, sort)
def runctx(statement, globals, locals, filename=None, sort=-1):
"""Run statement under profiler, supplying your own globals and locals,
optionally saving results in filename.
statement and filename have the same semantics as profile.run
"""
return _Utils(Profile).runctx(statement, globals, locals, filename, sort)
class Profile:
"""Profiler class.
self.cur is always a tuple. Each such tuple corresponds to a stack
frame that is currently active (self.cur[-2]). The following are the
definitions of its members. We use this external "parallel stack" to
avoid contaminating the program that we are profiling. (old profiler
used to write into the frames local dictionary!!) Derived classes
can change the definition of some entries, as long as they leave
[-2:] intact (frame and previous tuple). In case an internal error is
detected, the -3 element is used as the function name.
[ 0] = Time that needs to be charged to the parent frame's function.
It is used so that a function call will not have to access the
timing data for the parent frame.
[ 1] = Total time spent in this frame's function, excluding time in
subfunctions (this latter is tallied in cur[2]).
[ 2] = Total time spent in subfunctions, excluding time executing the
frame's function (this latter is tallied in cur[1]).
[-3] = Name of the function that corresponds to this frame.
[-2] = Actual frame that we correspond to (used to sync exception handling).
[-1] = Our parent 6-tuple (corresponds to frame.f_back).
Timing data for each function is stored as a 5-tuple in the dictionary
self.timings[]. The index is always the name stored in self.cur[-3].
The following are the definitions of the members:
[0] = The number of times this function was called, not counting direct
or indirect recursion,
[1] = Number of times this function appears on the stack, minus one
[2] = Total time spent internal to this function
[3] = Cumulative time that this function was present on the stack. In
non-recursive functions, this is the total execution time from start
to finish of each invocation of a function, including time spent in
all subfunctions.
[4] = A dictionary indicating for each function name, the number of times
it was called by us.
"""
bias = 0 # calibration constant
def __init__(self, timer=None, bias=None):
self.timings = {}
self.cur = None
self.cmd = ""
self.c_func_name = ""
if bias is None:
bias = self.bias
self.bias = bias # Materialize in local dict for lookup speed.
if not timer:
self.timer = self.get_time = time.process_time
self.dispatcher = self.trace_dispatch_i
else:
self.timer = timer
t = self.timer() # test out timer function
try:
length = len(t)
except TypeError:
self.get_time = timer
self.dispatcher = self.trace_dispatch_i
else:
if length == 2:
self.dispatcher = self.trace_dispatch
else:
self.dispatcher = self.trace_dispatch_l
# This get_time() implementation needs to be defined
# here to capture the passed-in timer in the parameter
# list (for performance). Note that we can't assume
# the timer() result contains two values in all
# cases.
def get_time_timer(timer=timer, sum=sum):
return sum(timer())
self.get_time = get_time_timer
self.t = self.get_time()
self.simulate_call('profiler')
# Heavily optimized dispatch routine for time.process_time() timer
def trace_dispatch(self, frame, event, arg):
timer = self.timer
t = timer()
t = t[0] + t[1] - self.t - self.bias
if event == "c_call":
self.c_func_name = arg.__name__
if self.dispatch[event](self, frame,t):
t = timer()
self.t = t[0] + t[1]
else:
r = timer()
self.t = r[0] + r[1] - t # put back unrecorded delta
# Dispatch routine for best timer program (return = scalar, fastest if
# an integer but float works too -- and time.process_time() relies on that).
def trace_dispatch_i(self, frame, event, arg):
timer = self.timer
t = timer() - self.t - self.bias
if event == "c_call":
self.c_func_name = arg.__name__
if self.dispatch[event](self, frame, t):
self.t = timer()
else:
self.t = timer() - t # put back unrecorded delta
# Dispatch routine for macintosh (timer returns time in ticks of
# 1/60th second)
def trace_dispatch_mac(self, frame, event, arg):
timer = self.timer
t = timer()/60.0 - self.t - self.bias
if event == "c_call":
self.c_func_name = arg.__name__
if self.dispatch[event](self, frame, t):
self.t = timer()/60.0
else:
self.t = timer()/60.0 - t # put back unrecorded delta
# SLOW generic dispatch routine for timer returning lists of numbers
def trace_dispatch_l(self, frame, event, arg):
get_time = self.get_time
t = get_time() - self.t - self.bias
if event == "c_call":
self.c_func_name = arg.__name__
if self.dispatch[event](self, frame, t):
self.t = get_time()
else:
self.t = get_time() - t # put back unrecorded delta
# In the event handlers, the first 3 elements of self.cur are unpacked
# into vrbls w/ 3-letter names. The last two characters are meant to be
# mnemonic:
# _pt self.cur[0] "parent time" time to be charged to parent frame
# _it self.cur[1] "internal time" time spent directly in the function
# _et self.cur[2] "external time" time spent in subfunctions
def trace_dispatch_exception(self, frame, t):
rpt, rit, ret, rfn, rframe, rcur = self.cur
if (rframe is not frame) and rcur:
return self.trace_dispatch_return(rframe, t)
self.cur = rpt, rit+t, ret, rfn, rframe, rcur
return 1
def trace_dispatch_call(self, frame, t):
if self.cur and frame.f_back is not self.cur[-2]:
rpt, rit, ret, rfn, rframe, rcur = self.cur
if not isinstance(rframe, Profile.fake_frame):
assert rframe.f_back is frame.f_back, ("Bad call", rfn,
rframe, rframe.f_back,
frame, frame.f_back)
self.trace_dispatch_return(rframe, 0)
assert (self.cur is None or \
frame.f_back is self.cur[-2]), ("Bad call",
self.cur[-3])
fcode = frame.f_code
fn = (fcode.co_filename, fcode.co_firstlineno, fcode.co_name)
self.cur = (t, 0, 0, fn, frame, self.cur)
timings = self.timings
if fn in timings:
cc, ns, tt, ct, callers = timings[fn]
timings[fn] = cc, ns + 1, tt, ct, callers
else:
timings[fn] = 0, 0, 0, 0, {}
return 1
def trace_dispatch_c_call (self, frame, t):
fn = ("", 0, self.c_func_name)
self.cur = (t, 0, 0, fn, frame, self.cur)
timings = self.timings
if fn in timings:
cc, ns, tt, ct, callers = timings[fn]
timings[fn] = cc, ns+1, tt, ct, callers
else:
timings[fn] = 0, 0, 0, 0, {}
return 1
def trace_dispatch_return(self, frame, t):
if frame is not self.cur[-2]:
assert frame is self.cur[-2].f_back, ("Bad return", self.cur[-3])
self.trace_dispatch_return(self.cur[-2], 0)
# Prefix "r" means part of the Returning or exiting frame.
# Prefix "p" means part of the Previous or Parent or older frame.
rpt, rit, ret, rfn, frame, rcur = self.cur
rit = rit + t
frame_total = rit + ret
ppt, pit, pet, pfn, pframe, pcur = rcur
self.cur = ppt, pit + rpt, pet + frame_total, pfn, pframe, pcur
timings = self.timings
cc, ns, tt, ct, callers = timings[rfn]
if not ns:
# This is the only occurrence of the function on the stack.
# Else this is a (directly or indirectly) recursive call, and
# its cumulative time will get updated when the topmost call to
# it returns.
ct = ct + frame_total
cc = cc + 1
if pfn in callers:
callers[pfn] = callers[pfn] + 1 # hack: gather more
# stats such as the amount of time added to ct courtesy
# of this specific call, and the contribution to cc
# courtesy of this call.
else:
callers[pfn] = 1
timings[rfn] = cc, ns - 1, tt + rit, ct, callers
return 1
dispatch = {
"call": trace_dispatch_call,
"exception": trace_dispatch_exception,
"return": trace_dispatch_return,
"c_call": trace_dispatch_c_call,
"c_exception": trace_dispatch_return, # the C function returned
"c_return": trace_dispatch_return,
}
# The next few functions play with self.cmd. By carefully preloading
# our parallel stack, we can force the profiled result to include
# an arbitrary string as the name of the calling function.
# We use self.cmd as that string, and the resulting stats look
# very nice :-).
def set_cmd(self, cmd):
if self.cur[-1]: return # already set
self.cmd = cmd
self.simulate_call(cmd)
class fake_code:
def __init__(self, filename, line, name):
self.co_filename = filename
self.co_line = line
self.co_name = name
self.co_firstlineno = 0
def __repr__(self):
return repr((self.co_filename, self.co_line, self.co_name))
class fake_frame:
def __init__(self, code, prior):
self.f_code = code
self.f_back = prior
def simulate_call(self, name):
code = self.fake_code('profile', 0, name)
if self.cur:
pframe = self.cur[-2]
else:
pframe = None
frame = self.fake_frame(code, pframe)
self.dispatch['call'](self, frame, 0)
# collect stats from pending stack, including getting final
# timings for self.cmd frame.
def simulate_cmd_complete(self):
get_time = self.get_time
t = get_time() - self.t
while self.cur[-1]:
# We *can* cause assertion errors here if
# dispatch_trace_return checks for a frame match!
self.dispatch['return'](self, self.cur[-2], t)
t = 0
self.t = get_time() - t
def print_stats(self, sort=-1):
import pstats
if not isinstance(sort, tuple):
sort = (sort,)
pstats.Stats(self).strip_dirs().sort_stats(*sort).print_stats()
def dump_stats(self, file):
with open(file, 'wb') as f:
self.create_stats()
marshal.dump(self.stats, f)
def create_stats(self):
self.simulate_cmd_complete()
self.snapshot_stats()
def snapshot_stats(self):
self.stats = {}
for func, (cc, ns, tt, ct, callers) in self.timings.items():
callers = callers.copy()
nc = 0
for callcnt in callers.values():
nc += callcnt
self.stats[func] = cc, nc, tt, ct, callers
# The following two methods can be called by clients to use
# a profiler to profile a statement, given as a string.
def run(self, cmd):
import __main__
dict = __main__.__dict__
return self.runctx(cmd, dict, dict)
def runctx(self, cmd, globals, locals):
self.set_cmd(cmd)
sys.setprofile(self.dispatcher)
try:
exec(cmd, globals, locals)
finally:
sys.setprofile(None)
return self
# This method is more useful to profile a single function call.
def runcall(self, func, /, *args, **kw):
self.set_cmd(repr(func))
sys.setprofile(self.dispatcher)
try:
return func(*args, **kw)
finally:
sys.setprofile(None)
#******************************************************************
# The following calculates the overhead for using a profiler. The
# problem is that it takes a fair amount of time for the profiler
# to stop the stopwatch (from the time it receives an event).
# Similarly, there is a delay from the time that the profiler
# re-starts the stopwatch before the user's code really gets to
# continue. The following code tries to measure the difference on
# a per-event basis.
#
# Note that this difference is only significant if there are a lot of
# events, and relatively little user code per event. For example,
# code with small functions will typically benefit from having the
# profiler calibrated for the current platform. This *could* be
# done on the fly during init() time, but it is not worth the
# effort. Also note that if too large a value specified, then
# execution time on some functions will actually appear as a
# negative number. It is *normal* for some functions (with very
# low call counts) to have such negative stats, even if the
# calibration figure is "correct."
#
# One alternative to profile-time calibration adjustments (i.e.,
# adding in the magic little delta during each event) is to track
# more carefully the number of events (and cumulatively, the number
# of events during sub functions) that are seen. If this were
# done, then the arithmetic could be done after the fact (i.e., at
# display time). Currently, we track only call/return events.
# These values can be deduced by examining the callees and callers
# vectors for each functions. Hence we *can* almost correct the
# internal time figure at print time (note that we currently don't
# track exception event processing counts). Unfortunately, there
# is currently no similar information for cumulative sub-function
# time. It would not be hard to "get all this info" at profiler
# time. Specifically, we would have to extend the tuples to keep
# counts of this in each frame, and then extend the defs of timing
# tuples to include the significant two figures. I'm a bit fearful
# that this additional feature will slow the heavily optimized
# event/time ratio (i.e., the profiler would run slower, fur a very
# low "value added" feature.)
#**************************************************************
def calibrate(self, m, verbose=0):
if self.__class__ is not Profile:
raise TypeError("Subclasses must override .calibrate().")
saved_bias = self.bias
self.bias = 0
try:
return self._calibrate_inner(m, verbose)
finally:
self.bias = saved_bias
def _calibrate_inner(self, m, verbose):
get_time = self.get_time
# Set up a test case to be run with and without profiling. Include
# lots of calls, because we're trying to quantify stopwatch overhead.
# Do not raise any exceptions, though, because we want to know
# exactly how many profile events are generated (one call event, +
# one return event, per Python-level call).
def f1(n):
for i in range(n):
x = 1
def f(m, f1=f1):
for i in range(m):
f1(100)
f(m) # warm up the cache
# elapsed_noprofile <- time f(m) takes without profiling.
t0 = get_time()
f(m)
t1 = get_time()
elapsed_noprofile = t1 - t0
if verbose:
print("elapsed time without profiling =", elapsed_noprofile)
# elapsed_profile <- time f(m) takes with profiling. The difference
# is profiling overhead, only some of which the profiler subtracts
# out on its own.
p = Profile()
t0 = get_time()
p.runctx('f(m)', globals(), locals())
t1 = get_time()
elapsed_profile = t1 - t0
if verbose:
print("elapsed time with profiling =", elapsed_profile)
# reported_time <- "CPU seconds" the profiler charged to f and f1.
total_calls = 0.0
reported_time = 0.0
for (filename, line, funcname), (cc, ns, tt, ct, callers) in \
p.timings.items():
if funcname in ("f", "f1"):
total_calls += cc
reported_time += tt
if verbose:
print("'CPU seconds' profiler reported =", reported_time)
print("total # calls =", total_calls)
if total_calls != m + 1:
raise ValueError("internal error: total calls = %d" % total_calls)
# reported_time - elapsed_noprofile = overhead the profiler wasn't
# able to measure. Divide by twice the number of calls (since there
# are two profiler events per call in this test) to get the hidden
# overhead per event.
mean = (reported_time - elapsed_noprofile) / 2.0 / total_calls
if verbose:
print("mean stopwatch overhead per profile event =", mean)
return mean
#****************************************************************************
def main():
import os
from optparse import OptionParser
usage = "profile.py [-o output_file_path] [-s sort] [-m module | scriptfile] [arg] ..."
parser = OptionParser(usage=usage)
parser.allow_interspersed_args = False
parser.add_option('-o', '--outfile', dest="outfile",
help="Save stats to <outfile>", default=None)
parser.add_option('-m', dest="module", action="store_true",
help="Profile a library module.", default=False)
parser.add_option('-s', '--sort', dest="sort",
help="Sort order when printing to stdout, based on pstats.Stats class",
default=-1)
if not sys.argv[1:]:
parser.print_usage()
sys.exit(2)
(options, args) = parser.parse_args()
sys.argv[:] = args
# The script that we're profiling may chdir, so capture the absolute path
# to the output file at startup.
if options.outfile is not None:
options.outfile = os.path.abspath(options.outfile)
if len(args) > 0:
if options.module:
import runpy
code = "run_module(modname, run_name='__main__')"
globs = {
'run_module': runpy.run_module,
'modname': args[0]
}
else:
progname = args[0]
sys.path.insert(0, os.path.dirname(progname))
with io.open_code(progname) as fp:
code = compile(fp.read(), progname, 'exec')
spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
origin=progname)
globs = {
'__spec__': spec,
'__file__': spec.origin,
'__name__': spec.name,
'__package__': None,
'__cached__': None,
}
try:
runctx(code, globs, None, options.outfile, options.sort)
except BrokenPipeError as exc:
# Prevent "Exception ignored" during interpreter shutdown.
sys.stdout = None
sys.exit(exc.errno)
else:
parser.print_usage()
return parser
# When invoked as main program, invoke the profiler on a script
if __name__ == '__main__':
main()

777
Lib/pstats.py vendored
View File

@@ -1,777 +0,0 @@
"""Class for printing reports on profiled python code."""
# Written by James Roskind
# Based on prior profile module by Sjoerd Mullender...
# which was hacked somewhat by: Guido van Rossum
# Copyright Disney Enterprises, Inc. All Rights Reserved.
# Licensed to PSF under a Contributor Agreement
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
import sys
import os
import time
import marshal
import re
from enum import StrEnum, _simple_enum
from functools import cmp_to_key
from dataclasses import dataclass
__all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]
@_simple_enum(StrEnum)
class SortKey:
CALLS = 'calls', 'ncalls'
CUMULATIVE = 'cumulative', 'cumtime'
FILENAME = 'filename', 'module'
LINE = 'line'
NAME = 'name'
NFL = 'nfl'
PCALLS = 'pcalls'
STDNAME = 'stdname'
TIME = 'time', 'tottime'
def __new__(cls, *values):
value = values[0]
obj = str.__new__(cls, value)
obj._value_ = value
for other_value in values[1:]:
cls._value2member_map_[other_value] = obj
obj._all_values = values
return obj
@dataclass(unsafe_hash=True)
class FunctionProfile:
ncalls: str
tottime: float
percall_tottime: float
cumtime: float
percall_cumtime: float
file_name: str
line_number: int
@dataclass(unsafe_hash=True)
class StatsProfile:
'''Class for keeping track of an item in inventory.'''
total_tt: float
func_profiles: dict[str, FunctionProfile]
class Stats:
"""This class is used for creating reports from data generated by the
Profile class. It is a "friend" of that class, and imports data either
by direct access to members of Profile class, or by reading in a dictionary
that was emitted (via marshal) from the Profile class.
The big change from the previous Profiler (in terms of raw functionality)
is that an "add()" method has been provided to combine Stats from
several distinct profile runs. Both the constructor and the add()
method now take arbitrarily many file names as arguments.
All the print methods now take an argument that indicates how many lines
to print. If the arg is a floating-point number between 0 and 1.0, then
it is taken as a decimal percentage of the available lines to be printed
(e.g., .1 means print 10% of all available lines). If it is an integer,
it is taken to mean the number of lines of data that you wish to have
printed.
The sort_stats() method now processes some additional options (i.e., in
addition to the old -1, 0, 1, or 2 that are respectively interpreted as
'stdname', 'calls', 'time', and 'cumulative'). It takes either an
arbitrary number of quoted strings or SortKey enum to select the sort
order.
For example sort_stats('time', 'name') or sort_stats(SortKey.TIME,
SortKey.NAME) sorts on the major key of 'internal function time', and on
the minor key of 'the name of the function'. Look at the two tables in
sort_stats() and get_sort_arg_defs(self) for more examples.
All methods return self, so you can string together commands like:
Stats('foo', 'goo').strip_dirs().sort_stats('calls').\
print_stats(5).print_callers(5)
"""
def __init__(self, *args, stream=None):
self.stream = stream or sys.stdout
if not len(args):
arg = None
else:
arg = args[0]
args = args[1:]
self.init(arg)
self.add(*args)
def init(self, arg):
self.all_callees = None # calc only if needed
self.files = []
self.fcn_list = None
self.total_tt = 0
self.total_calls = 0
self.prim_calls = 0
self.max_name_len = 0
self.top_level = set()
self.stats = {}
self.sort_arg_dict = {}
self.load_stats(arg)
try:
self.get_top_level_stats()
except Exception:
print("Invalid timing data %s" %
(self.files[-1] if self.files else ''), file=self.stream)
raise
def load_stats(self, arg):
if arg is None:
self.stats = {}
return
elif isinstance(arg, str):
with open(arg, 'rb') as f:
self.stats = marshal.load(f)
try:
file_stats = os.stat(arg)
arg = time.ctime(file_stats.st_mtime) + " " + arg
except: # in case this is not unix
pass
self.files = [arg]
elif hasattr(arg, 'create_stats'):
arg.create_stats()
self.stats = arg.stats
arg.stats = {}
if not self.stats:
raise TypeError("Cannot create or construct a %r object from %r"
% (self.__class__, arg))
return
def get_top_level_stats(self):
for func, (cc, nc, tt, ct, callers) in self.stats.items():
self.total_calls += nc
self.prim_calls += cc
self.total_tt += tt
if ("jprofile", 0, "profiler") in callers:
self.top_level.add(func)
if len(func_std_string(func)) > self.max_name_len:
self.max_name_len = len(func_std_string(func))
def add(self, *arg_list):
if not arg_list:
return self
for item in reversed(arg_list):
if type(self) != type(item):
item = Stats(item)
self.files += item.files
self.total_calls += item.total_calls
self.prim_calls += item.prim_calls
self.total_tt += item.total_tt
for func in item.top_level:
self.top_level.add(func)
if self.max_name_len < item.max_name_len:
self.max_name_len = item.max_name_len
self.fcn_list = None
for func, stat in item.stats.items():
if func in self.stats:
old_func_stat = self.stats[func]
else:
old_func_stat = (0, 0, 0, 0, {},)
self.stats[func] = add_func_stats(old_func_stat, stat)
return self
def dump_stats(self, filename):
"""Write the profile data to a file we know how to load back."""
with open(filename, 'wb') as f:
marshal.dump(self.stats, f)
# list the tuple indices and directions for sorting,
# along with some printable description
sort_arg_dict_default = {
"calls" : (((1,-1), ), "call count"),
"ncalls" : (((1,-1), ), "call count"),
"cumtime" : (((3,-1), ), "cumulative time"),
"cumulative": (((3,-1), ), "cumulative time"),
"filename" : (((4, 1), ), "file name"),
"line" : (((5, 1), ), "line number"),
"module" : (((4, 1), ), "file name"),
"name" : (((6, 1), ), "function name"),
"nfl" : (((6, 1),(4, 1),(5, 1),), "name/file/line"),
"pcalls" : (((0,-1), ), "primitive call count"),
"stdname" : (((7, 1), ), "standard name"),
"time" : (((2,-1), ), "internal time"),
"tottime" : (((2,-1), ), "internal time"),
}
def get_sort_arg_defs(self):
"""Expand all abbreviations that are unique."""
if not self.sort_arg_dict:
self.sort_arg_dict = dict = {}
bad_list = {}
for word, tup in self.sort_arg_dict_default.items():
fragment = word
while fragment:
if fragment in dict:
bad_list[fragment] = 0
break
dict[fragment] = tup
fragment = fragment[:-1]
for word in bad_list:
del dict[word]
return self.sort_arg_dict
def sort_stats(self, *field):
if not field:
self.fcn_list = 0
return self
if len(field) == 1 and isinstance(field[0], int):
# Be compatible with old profiler
field = [ {-1: "stdname",
0: "calls",
1: "time",
2: "cumulative"}[field[0]] ]
elif len(field) >= 2:
for arg in field[1:]:
if type(arg) != type(field[0]):
raise TypeError("Can't have mixed argument type")
sort_arg_defs = self.get_sort_arg_defs()
sort_tuple = ()
self.sort_type = ""
connector = ""
for word in field:
if isinstance(word, SortKey):
word = word.value
sort_tuple = sort_tuple + sort_arg_defs[word][0]
self.sort_type += connector + sort_arg_defs[word][1]
connector = ", "
stats_list = []
for func, (cc, nc, tt, ct, callers) in self.stats.items():
stats_list.append((cc, nc, tt, ct) + func +
(func_std_string(func), func))
stats_list.sort(key=cmp_to_key(TupleComp(sort_tuple).compare))
self.fcn_list = fcn_list = []
for tuple in stats_list:
fcn_list.append(tuple[-1])
return self
def reverse_order(self):
if self.fcn_list:
self.fcn_list.reverse()
return self
def strip_dirs(self):
oldstats = self.stats
self.stats = newstats = {}
max_name_len = 0
for func, (cc, nc, tt, ct, callers) in oldstats.items():
newfunc = func_strip_path(func)
if len(func_std_string(newfunc)) > max_name_len:
max_name_len = len(func_std_string(newfunc))
newcallers = {}
for func2, caller in callers.items():
newcallers[func_strip_path(func2)] = caller
if newfunc in newstats:
newstats[newfunc] = add_func_stats(
newstats[newfunc],
(cc, nc, tt, ct, newcallers))
else:
newstats[newfunc] = (cc, nc, tt, ct, newcallers)
old_top = self.top_level
self.top_level = new_top = set()
for func in old_top:
new_top.add(func_strip_path(func))
self.max_name_len = max_name_len
self.fcn_list = None
self.all_callees = None
return self
def calc_callees(self):
if self.all_callees:
return
self.all_callees = all_callees = {}
for func, (cc, nc, tt, ct, callers) in self.stats.items():
if not func in all_callees:
all_callees[func] = {}
for func2, caller in callers.items():
if not func2 in all_callees:
all_callees[func2] = {}
all_callees[func2][func] = caller
return
#******************************************************************
# The following functions support actual printing of reports
#******************************************************************
# Optional "amount" is either a line count, or a percentage of lines.
def eval_print_amount(self, sel, list, msg):
new_list = list
if isinstance(sel, str):
try:
rex = re.compile(sel)
except re.PatternError:
msg += " <Invalid regular expression %r>\n" % sel
return new_list, msg
new_list = []
for func in list:
if rex.search(func_std_string(func)):
new_list.append(func)
else:
count = len(list)
if isinstance(sel, float) and 0.0 <= sel < 1.0:
count = int(count * sel + .5)
new_list = list[:count]
elif isinstance(sel, int) and 0 <= sel < count:
count = sel
new_list = list[:count]
if len(list) != len(new_list):
msg += " List reduced from %r to %r due to restriction <%r>\n" % (
len(list), len(new_list), sel)
return new_list, msg
def get_stats_profile(self):
"""This method returns an instance of StatsProfile, which contains a mapping
of function names to instances of FunctionProfile. Each FunctionProfile
instance holds information related to the function's profile such as how
long the function took to run, how many times it was called, etc...
"""
func_list = self.fcn_list[:] if self.fcn_list else list(self.stats.keys())
if not func_list:
return StatsProfile(0, {})
total_tt = float(f8(self.total_tt))
func_profiles = {}
stats_profile = StatsProfile(total_tt, func_profiles)
for func in func_list:
cc, nc, tt, ct, callers = self.stats[func]
file_name, line_number, func_name = func
ncalls = str(nc) if nc == cc else (str(nc) + '/' + str(cc))
tottime = float(f8(tt))
percall_tottime = -1 if nc == 0 else float(f8(tt/nc))
cumtime = float(f8(ct))
percall_cumtime = -1 if cc == 0 else float(f8(ct/cc))
func_profile = FunctionProfile(
ncalls,
tottime, # time spent in this function alone
percall_tottime,
cumtime, # time spent in the function plus all functions that this function called,
percall_cumtime,
file_name,
line_number
)
func_profiles[func_name] = func_profile
return stats_profile
def get_print_list(self, sel_list):
width = self.max_name_len
if self.fcn_list:
stat_list = self.fcn_list[:]
msg = " Ordered by: " + self.sort_type + '\n'
else:
stat_list = list(self.stats.keys())
msg = " Random listing order was used\n"
for selection in sel_list:
stat_list, msg = self.eval_print_amount(selection, stat_list, msg)
count = len(stat_list)
if not stat_list:
return 0, stat_list
print(msg, file=self.stream)
if count < len(self.stats):
width = 0
for func in stat_list:
if len(func_std_string(func)) > width:
width = len(func_std_string(func))
return width+2, stat_list
def print_stats(self, *amount):
for filename in self.files:
print(filename, file=self.stream)
if self.files:
print(file=self.stream)
indent = ' ' * 8
for func in self.top_level:
print(indent, func_get_function_name(func), file=self.stream)
print(indent, self.total_calls, "function calls", end=' ', file=self.stream)
if self.total_calls != self.prim_calls:
print("(%d primitive calls)" % self.prim_calls, end=' ', file=self.stream)
print("in %.3f seconds" % self.total_tt, file=self.stream)
print(file=self.stream)
width, list = self.get_print_list(amount)
if list:
self.print_title()
for func in list:
self.print_line(func)
print(file=self.stream)
print(file=self.stream)
return self
def print_callees(self, *amount):
width, list = self.get_print_list(amount)
if list:
self.calc_callees()
self.print_call_heading(width, "called...")
for func in list:
if func in self.all_callees:
self.print_call_line(width, func, self.all_callees[func])
else:
self.print_call_line(width, func, {})
print(file=self.stream)
print(file=self.stream)
return self
def print_callers(self, *amount):
width, list = self.get_print_list(amount)
if list:
self.print_call_heading(width, "was called by...")
for func in list:
cc, nc, tt, ct, callers = self.stats[func]
self.print_call_line(width, func, callers, "<-")
print(file=self.stream)
print(file=self.stream)
return self
def print_call_heading(self, name_size, column_title):
print("Function ".ljust(name_size) + column_title, file=self.stream)
# print sub-header only if we have new-style callers
subheader = False
for cc, nc, tt, ct, callers in self.stats.values():
if callers:
value = next(iter(callers.values()))
subheader = isinstance(value, tuple)
break
if subheader:
print(" "*name_size + " ncalls tottime cumtime", file=self.stream)
def print_call_line(self, name_size, source, call_dict, arrow="->"):
print(func_std_string(source).ljust(name_size) + arrow, end=' ', file=self.stream)
if not call_dict:
print(file=self.stream)
return
clist = sorted(call_dict.keys())
indent = ""
for func in clist:
name = func_std_string(func)
value = call_dict[func]
if isinstance(value, tuple):
nc, cc, tt, ct = value
if nc != cc:
substats = '%d/%d' % (nc, cc)
else:
substats = '%d' % (nc,)
substats = '%s %s %s %s' % (substats.rjust(7+2*len(indent)),
f8(tt), f8(ct), name)
left_width = name_size + 1
else:
substats = '%s(%r) %s' % (name, value, f8(self.stats[func][3]))
left_width = name_size + 3
print(indent*left_width + substats, file=self.stream)
indent = " "
def print_title(self):
print(' ncalls tottime percall cumtime percall', end=' ', file=self.stream)
print('filename:lineno(function)', file=self.stream)
def print_line(self, func): # hack: should print percentages
cc, nc, tt, ct, callers = self.stats[func]
c = str(nc)
if nc != cc:
c = c + '/' + str(cc)
print(c.rjust(9), end=' ', file=self.stream)
print(f8(tt), end=' ', file=self.stream)
if nc == 0:
print(' '*8, end=' ', file=self.stream)
else:
print(f8(tt/nc), end=' ', file=self.stream)
print(f8(ct), end=' ', file=self.stream)
if cc == 0:
print(' '*8, end=' ', file=self.stream)
else:
print(f8(ct/cc), end=' ', file=self.stream)
print(func_std_string(func), file=self.stream)
class TupleComp:
"""This class provides a generic function for comparing any two tuples.
Each instance records a list of tuple-indices (from most significant
to least significant), and sort direction (ascending or descending) for
each tuple-index. The compare functions can then be used as the function
argument to the system sort() function when a list of tuples need to be
sorted in the instances order."""
def __init__(self, comp_select_list):
self.comp_select_list = comp_select_list
def compare (self, left, right):
for index, direction in self.comp_select_list:
l = left[index]
r = right[index]
if l < r:
return -direction
if l > r:
return direction
return 0
#**************************************************************************
# func_name is a triple (file:string, line:int, name:string)
def func_strip_path(func_name):
filename, line, name = func_name
return os.path.basename(filename), line, name
def func_get_function_name(func):
return func[2]
def func_std_string(func_name): # match what old profile produced
if func_name[:2] == ('~', 0):
# special case for built-in functions
name = func_name[2]
if name.startswith('<') and name.endswith('>'):
return '{%s}' % name[1:-1]
else:
return name
else:
return "%s:%d(%s)" % func_name
#**************************************************************************
# The following functions combine statistics for pairs functions.
# The bulk of the processing involves correctly handling "call" lists,
# such as callers and callees.
#**************************************************************************
def add_func_stats(target, source):
"""Add together all the stats for two profile entries."""
cc, nc, tt, ct, callers = source
t_cc, t_nc, t_tt, t_ct, t_callers = target
return (cc+t_cc, nc+t_nc, tt+t_tt, ct+t_ct,
add_callers(t_callers, callers))
def add_callers(target, source):
"""Combine two caller lists in a single list."""
new_callers = {}
for func, caller in target.items():
new_callers[func] = caller
for func, caller in source.items():
if func in new_callers:
if isinstance(caller, tuple):
# format used by cProfile
new_callers[func] = tuple(i + j for i, j in zip(caller, new_callers[func]))
else:
# format used by profile
new_callers[func] += caller
else:
new_callers[func] = caller
return new_callers
def count_calls(callers):
"""Sum the caller statistics to get total number of calls received."""
nc = 0
for calls in callers.values():
nc += calls
return nc
#**************************************************************************
# The following functions support printing of reports
#**************************************************************************
def f8(x):
return "%8.3f" % x
#**************************************************************************
# Statistics browser added by ESR, April 2001
#**************************************************************************
if __name__ == '__main__':
import cmd
try:
import readline # noqa: F401
except ImportError:
pass
class ProfileBrowser(cmd.Cmd):
def __init__(self, profile=None):
cmd.Cmd.__init__(self)
self.prompt = "% "
self.stats = None
self.stream = sys.stdout
if profile is not None:
self.do_read(profile)
def generic(self, fn, line):
args = line.split()
processed = []
for term in args:
try:
processed.append(int(term))
continue
except ValueError:
pass
try:
frac = float(term)
if frac > 1 or frac < 0:
print("Fraction argument must be in [0, 1]", file=self.stream)
continue
processed.append(frac)
continue
except ValueError:
pass
processed.append(term)
if self.stats:
getattr(self.stats, fn)(*processed)
else:
print("No statistics object is loaded.", file=self.stream)
return 0
def generic_help(self):
print("Arguments may be:", file=self.stream)
print("* An integer maximum number of entries to print.", file=self.stream)
print("* A decimal fractional number between 0 and 1, controlling", file=self.stream)
print(" what fraction of selected entries to print.", file=self.stream)
print("* A regular expression; only entries with function names", file=self.stream)
print(" that match it are printed.", file=self.stream)
def do_add(self, line):
if self.stats:
try:
self.stats.add(line)
except OSError as e:
print("Failed to load statistics for %s: %s" % (line, e), file=self.stream)
else:
print("No statistics object is loaded.", file=self.stream)
return 0
def help_add(self):
print("Add profile info from given file to current statistics object.", file=self.stream)
def do_callees(self, line):
return self.generic('print_callees', line)
def help_callees(self):
print("Print callees statistics from the current stat object.", file=self.stream)
self.generic_help()
def do_callers(self, line):
return self.generic('print_callers', line)
def help_callers(self):
print("Print callers statistics from the current stat object.", file=self.stream)
self.generic_help()
def do_EOF(self, line):
print("", file=self.stream)
return 1
def help_EOF(self):
print("Leave the profile browser.", file=self.stream)
def do_quit(self, line):
return 1
def help_quit(self):
print("Leave the profile browser.", file=self.stream)
def do_read(self, line):
if line:
try:
self.stats = Stats(line)
except OSError as err:
print(err.args[1], file=self.stream)
return
except Exception as err:
print(err.__class__.__name__ + ':', err, file=self.stream)
return
self.prompt = line + "% "
elif len(self.prompt) > 2:
line = self.prompt[:-2]
self.do_read(line)
else:
print("No statistics object is current -- cannot reload.", file=self.stream)
return 0
def help_read(self):
print("Read in profile data from a specified file.", file=self.stream)
print("Without argument, reload the current file.", file=self.stream)
def do_reverse(self, line):
if self.stats:
self.stats.reverse_order()
else:
print("No statistics object is loaded.", file=self.stream)
return 0
def help_reverse(self):
print("Reverse the sort order of the profiling report.", file=self.stream)
def do_sort(self, line):
if not self.stats:
print("No statistics object is loaded.", file=self.stream)
return
abbrevs = self.stats.get_sort_arg_defs()
if line and all((x in abbrevs) for x in line.split()):
self.stats.sort_stats(*line.split())
else:
print("Valid sort keys (unique prefixes are accepted):", file=self.stream)
for (key, value) in Stats.sort_arg_dict_default.items():
print("%s -- %s" % (key, value[1]), file=self.stream)
return 0
def help_sort(self):
print("Sort profile data according to specified keys.", file=self.stream)
print("(Typing `sort' without arguments lists valid keys.)", file=self.stream)
def complete_sort(self, text, *args):
return [a for a in Stats.sort_arg_dict_default if a.startswith(text)]
def do_stats(self, line):
return self.generic('print_stats', line)
def help_stats(self):
print("Print statistics from the current stat object.", file=self.stream)
self.generic_help()
def do_strip(self, line):
if self.stats:
self.stats.strip_dirs()
else:
print("No statistics object is loaded.", file=self.stream)
def help_strip(self):
print("Strip leading path information from filenames in the report.", file=self.stream)
def help_help(self):
print("Show help for a given command.", file=self.stream)
def postcmd(self, stop, line):
if stop:
return stop
return None
if len(sys.argv) > 1:
initprofile = sys.argv[1]
else:
initprofile = None
try:
browser = ProfileBrowser(initprofile)
for profile in sys.argv[2:]:
browser.do_add(profile)
print("Welcome to the profile statistics browser.", file=browser.stream)
browser.cmdloop()
print("Goodbye.", file=browser.stream)
except KeyboardInterrupt:
pass
# That's all, folks.

51
Lib/pty.py vendored
View File

@@ -32,19 +32,28 @@ def openpty():
except (AttributeError, OSError):
pass
master_fd, slave_name = _open_terminal()
slave_fd = os.open(slave_name, os.O_RDWR)
try:
from fcntl import ioctl, I_PUSH
except ImportError:
return master_fd, slave_fd
try:
ioctl(slave_fd, I_PUSH, "ptem")
ioctl(slave_fd, I_PUSH, "ldterm")
except OSError:
pass
slave_fd = slave_open(slave_name)
return master_fd, slave_fd
def master_open():
"""master_open() -> (master_fd, slave_name)
Open a pty master and return the fd, and the filename of the slave end.
Deprecated, use openpty() instead."""
import warnings
warnings.warn("Use pty.openpty() instead.", DeprecationWarning, stacklevel=2) # Remove API in 3.14
try:
master_fd, slave_fd = os.openpty()
except (AttributeError, OSError):
pass
else:
slave_name = os.ttyname(slave_fd)
os.close(slave_fd)
return master_fd, slave_name
return _open_terminal()
def _open_terminal():
"""Open pty master and return (master_fd, tty_name)."""
for x in 'pqrstuvwxyzPQRST':
@@ -57,6 +66,26 @@ def _open_terminal():
return (fd, '/dev/tty' + x + y)
raise OSError('out of pty devices')
def slave_open(tty_name):
"""slave_open(tty_name) -> slave_fd
Open the pty slave and acquire the controlling terminal, returning
opened filedescriptor.
Deprecated, use openpty() instead."""
import warnings
warnings.warn("Use pty.openpty() instead.", DeprecationWarning, stacklevel=2) # Remove API in 3.14
result = os.open(tty_name, os.O_RDWR)
try:
from fcntl import ioctl, I_PUSH
except ImportError:
return result
try:
ioctl(result, I_PUSH, "ptem")
ioctl(result, I_PUSH, "ldterm")
except OSError:
pass
return result
def fork():
"""fork() -> (pid, master_fd)

View File

@@ -1,4 +1,4 @@
# Autogenerated by Sphinx on Sun May 10 13:21:26 2026
# Autogenerated by Sphinx on Tue Feb 3 17:32:13 2026
# as part of the release process.
module_docs = {

File diff suppressed because it is too large Load Diff

17
Lib/random.py vendored
View File

@@ -836,11 +836,7 @@ class Random(_random.Random):
if not c:
return x
while True:
try:
y += _floor(_log2(random()) / c) + 1
except ValueError:
# Reject case where random() returned 0.0
continue
y += _floor(_log2(random()) / c) + 1
if y > n:
return x
x += 1
@@ -848,8 +844,8 @@ class Random(_random.Random):
# BTRS: Transformed rejection with squeeze method by Wolfgang Hörmann
# https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.47.8407&rep=rep1&type=pdf
assert n*p >= 10.0 and p <= 0.5
setup_complete = False
spq = _sqrt(n * p * (1.0 - p)) # Standard deviation of the distribution
b = 1.15 + 2.53 * spq
a = -0.0873 + 0.0248 * b + 0.01 * p
@@ -864,23 +860,22 @@ class Random(_random.Random):
k = _floor((2.0 * a / us + b) * u + c)
if k < 0 or k > n:
continue
v = random()
# The early-out "squeeze" test substantially reduces
# the number of acceptance condition evaluations.
v = random()
if us >= 0.07 and v <= vr:
return k
# Acceptance-rejection test.
# Note, the original paper erroneously omits the call to log(v)
# when comparing to the log of the rescaled binomial distribution.
if not setup_complete:
alpha = (2.83 + 5.1 / b) * spq
lpq = _log(p / (1.0 - p))
m = _floor((n + 1) * p) # Mode of the distribution
h = _lgamma(m + 1) + _lgamma(n - m + 1)
setup_complete = True # Only needs to be done once
# Acceptance-rejection test.
# Note, the original paper erroneously omits the call to log(v)
# when comparing to the log of the rescaled binomial distribution.
v *= alpha / (a / (us * us) + b)
if _log(v) <= h - _lgamma(k + 1) - _lgamma(n - k + 1) + (k - m) * lpq:
return k

25
Lib/runpy.py vendored
View File

@@ -103,10 +103,8 @@ def _run_module_code(code, init_globals=None,
# Helper to get the full name, spec and code for a module
def _get_module_details(mod_name, error=ImportError):
# name= is only accepted by ImportError and its subclasses.
kwargs = {"name": mod_name} if issubclass(error, ImportError) else {}
if mod_name.startswith("."):
raise error("Relative module names not supported", **kwargs)
raise error("Relative module names not supported")
pkg_name, _, _ = mod_name.rpartition(".")
if pkg_name:
# Try importing the parent to avoid catching initialization errors
@@ -139,13 +137,12 @@ def _get_module_details(mod_name, error=ImportError):
if mod_name.endswith(".py"):
msg += (f". Try using '{mod_name[:-3]}' instead of "
f"'{mod_name}' as the module name.")
raise error(msg.format(mod_name, type(ex).__name__, ex),
**kwargs) from ex
raise error(msg.format(mod_name, type(ex).__name__, ex)) from ex
if spec is None:
raise error("No module named %s" % mod_name, **kwargs)
raise error("No module named %s" % mod_name)
if spec.submodule_search_locations is not None:
if mod_name == "__main__" or mod_name.endswith(".__main__"):
raise error("Cannot use package as __main__ module", **kwargs)
raise error("Cannot use package as __main__ module")
try:
pkg_main_name = mod_name + ".__main__"
return _get_module_details(pkg_main_name, error)
@@ -153,19 +150,17 @@ def _get_module_details(mod_name, error=ImportError):
if mod_name not in sys.modules:
raise # No module loaded; being a package is irrelevant
raise error(("%s; %r is a package and cannot " +
"be directly executed") %(e, mod_name),
**kwargs)
"be directly executed") %(e, mod_name))
loader = spec.loader
if loader is None:
raise error("%r is a namespace package and cannot be executed"
% mod_name,
**kwargs)
% mod_name)
try:
code = loader.get_code(mod_name)
except ImportError as e:
raise error(format(e), **kwargs) from e
raise error(format(e)) from e
if code is None:
raise error("No code object available for %s" % mod_name, **kwargs)
raise error("No code object available for %s" % mod_name)
return mod_name, spec, code
class _Error(Exception):
@@ -239,7 +234,6 @@ def _get_main_module_details(error=ImportError):
# Also moves the standard __main__ out of the way so that the
# preexisting __loader__ entry doesn't cause issues
main_name = "__main__"
kwargs = {"name": main_name} if issubclass(error, ImportError) else {}
saved_main = sys.modules[main_name]
del sys.modules[main_name]
try:
@@ -247,8 +241,7 @@ def _get_main_module_details(error=ImportError):
except ImportError as exc:
if main_name in str(exc):
raise error("can't find %r module in %r" %
(main_name, sys.path[0]),
**kwargs) from exc
(main_name, sys.path[0])) from exc
raise
finally:
sys.modules[main_name] = saved_main

24
Lib/shutil.py vendored
View File

@@ -1314,9 +1314,27 @@ def _unpack_zipfile(filename, extract_dir):
if not zipfile.is_zipfile(filename):
raise ReadError("%s is not a zip file" % filename)
with zipfile.ZipFile(filename) as zip:
zip._ignore_invalid_names = True
zip.extractall(extract_dir)
zip = zipfile.ZipFile(filename)
try:
for info in zip.infolist():
name = info.filename
# don't extract absolute paths or ones with .. in them
if name.startswith('/') or '..' in name:
continue
targetpath = os.path.join(extract_dir, *name.split('/'))
if not targetpath:
continue
_ensure_directory(targetpath)
if not name.endswith('/'):
# file
with zip.open(name, 'r') as source, \
open(targetpath, 'wb') as target:
copyfileobj(source, target)
finally:
zip.close()
def _unpack_tarfile(filename, extract_dir, *, filter=None):
"""Unpack tar/tar.gz/tar.bz2/tar.xz/tar.zst `filename` to `extract_dir`

2
Lib/site.py vendored
View File

@@ -290,7 +290,7 @@ def check_enableusersite():
# Copy of sysconfig._get_implementation()
def _get_implementation():
return 'RustPython' # XXX: RUSTPYTHON; for site-packages
return 'RustPython' # XXX: RustPython; for site-packages
# Copy of sysconfig._getuserbase()
def _getuserbase():

2
Lib/smtplib.py vendored
View File

@@ -251,6 +251,7 @@ class SMTP:
will be used.
"""
self._host = host
self.timeout = timeout
self.esmtp_features = {}
self.command_encoding = 'ascii'
@@ -341,7 +342,6 @@ class SMTP:
port = int(port)
except ValueError:
raise OSError("nonnumeric port")
self._host = host
if not port:
port = self.default_port
sys.audit("smtplib.connect", self, host, port)

28
Lib/socket.py vendored
View File

@@ -640,22 +640,18 @@ def _fallback_socketpair(family=AF_INET, type=SOCK_STREAM, proto=0):
# Authenticating avoids using a connection from something else
# able to connect to {host}:{port} instead of us.
# We expect only AF_INET and AF_INET6 families.
#
# Note that we skip this on WASI because on that platorm the client socket
# may not have finished connecting by the time we've reached this point (gh-146139).
if sys.platform != "wasi":
try:
if (
ssock.getsockname() != csock.getpeername()
or csock.getsockname() != ssock.getpeername()
):
raise ConnectionError("Unexpected peer connection")
except:
# getsockname() and getpeername() can fail
# if either socket isn't connected.
ssock.close()
csock.close()
raise
try:
if (
ssock.getsockname() != csock.getpeername()
or csock.getsockname() != ssock.getpeername()
):
raise ConnectionError("Unexpected peer connection")
except:
# getsockname() and getpeername() can fail
# if either socket isn't connected.
ssock.close()
csock.close()
raise
return (ssock, csock)

19
Lib/subprocess.py vendored
View File

@@ -351,16 +351,15 @@ def _args_from_interpreter_flags():
# -X options
if dev_mode:
args.extend(('-X', 'dev'))
for opt in sorted(xoptions):
if opt == 'dev':
# handled above via sys.flags.dev_mode
continue
value = xoptions[opt]
if value is True:
arg = opt
else:
arg = '%s=%s' % (opt, value)
args.extend(('-X', arg))
for opt in ('faulthandler', 'tracemalloc', 'importtime',
'frozen_modules', 'showrefcount', 'utf8', 'gil'):
if opt in xoptions:
value = xoptions[opt]
if value is True:
arg = opt
else:
arg = '%s=%s' % (opt, value)
args.extend(('-X', arg))
return args

View File

@@ -107,7 +107,7 @@ else:
_INSTALL_SCHEMES['venv'] = _INSTALL_SCHEMES['posix_venv']
def _get_implementation():
return 'RustPython' # XXX: RUSTPYTHON; For site-packages
return 'RustPython' # XXX: For site-packages
# NOTE: site.py has copy of this function.
# Sync it when modify this function.
@@ -698,19 +698,11 @@ def get_platform():
release = get_config_var("ANDROID_API_LEVEL")
# Wheel tags use the ABI names from Android's own tools.
# When Python is running on 32-bit ARM Android on a 64-bit ARM kernel,
# 'os.uname().machine' is 'armv8l'. Such devices run the same userspace
# code as 'armv7l' devices.
# During the build process of the Android testbed when targeting 32-bit ARM,
# '_PYTHON_HOST_PLATFORM' is 'arm-linux-androideabi', so 'machine' becomes
# 'arm'.
machine = {
"aarch64": "arm64_v8a",
"arm": "armeabi_v7a",
"armv7l": "armeabi_v7a",
"armv8l": "armeabi_v7a",
"i686": "x86",
"x86_64": "x86_64",
"i686": "x86",
"aarch64": "arm64_v8a",
"armv7l": "armeabi_v7a",
}[machine]
elif osname == "linux":
# At least on Linux/Intel, 'machine' is the processor --

29
Lib/tarfile.py vendored
View File

@@ -1278,20 +1278,6 @@ class TarInfo(object):
@classmethod
def frombuf(cls, buf, encoding, errors):
"""Construct a TarInfo object from a 512 byte bytes object.
To support the old v7 tar format AREGTYPE headers are
transformed to DIRTYPE headers if their name ends in '/'.
"""
return cls._frombuf(buf, encoding, errors)
@classmethod
def _frombuf(cls, buf, encoding, errors, *, dircheck=True):
"""Construct a TarInfo object from a 512 byte bytes object.
If ``dircheck`` is set to ``True`` then ``AREGTYPE`` headers will
be normalized to ``DIRTYPE`` if the name ends in a trailing slash.
``dircheck`` must be set to ``False`` if this function is called
on a follow-up header such as ``GNUTYPE_LONGNAME``.
"""
if len(buf) == 0:
raise EmptyHeaderError("empty header")
@@ -1322,7 +1308,7 @@ class TarInfo(object):
# Old V7 tar format represents a directory as a regular
# file with a trailing slash.
if dircheck and obj.type == AREGTYPE and obj.name.endswith("/"):
if obj.type == AREGTYPE and obj.name.endswith("/"):
obj.type = DIRTYPE
# The old GNU sparse format occupies some of the unused
@@ -1357,15 +1343,8 @@ class TarInfo(object):
"""Return the next TarInfo object from TarFile object
tarfile.
"""
return cls._fromtarfile(tarfile)
@classmethod
def _fromtarfile(cls, tarfile, *, dircheck=True):
"""
See dircheck documentation in _frombuf().
"""
buf = tarfile.fileobj.read(BLOCKSIZE)
obj = cls._frombuf(buf, tarfile.encoding, tarfile.errors, dircheck=dircheck)
obj = cls.frombuf(buf, tarfile.encoding, tarfile.errors)
obj.offset = tarfile.fileobj.tell() - BLOCKSIZE
return obj._proc_member(tarfile)
@@ -1423,7 +1402,7 @@ class TarInfo(object):
# Fetch the next header and process it.
try:
next = self._fromtarfile(tarfile, dircheck=False)
next = self.fromtarfile(tarfile)
except HeaderError as e:
raise SubsequentHeaderError(str(e)) from None
@@ -1558,7 +1537,7 @@ class TarInfo(object):
# Fetch the next header.
try:
next = self._fromtarfile(tarfile, dircheck=False)
next = self.fromtarfile(tarfile)
except HeaderError as e:
raise SubsequentHeaderError(str(e)) from None

36
Lib/tempfile.py vendored
View File

@@ -57,11 +57,10 @@ _bin_openflags = _text_openflags
if hasattr(_os, 'O_BINARY'):
_bin_openflags |= _os.O_BINARY
# This is more than enough.
# Each name contains over 40 random bits. Even with a million temporary
# files, the chance of a conflict is less than 1 in a million, and with
# 20 attempts, it is less than 1e-120.
TMP_MAX = 20
if hasattr(_os, 'TMP_MAX'):
TMP_MAX = _os.TMP_MAX
else:
TMP_MAX = 10000
# This variable _was_ unused for legacy reasons, see issue 10354.
# But as of 3.5 we actually use it at runtime so changing it would
@@ -197,7 +196,8 @@ def _get_default_tempdir(dirlist=None):
for dir in dirlist:
if dir != _os.curdir:
dir = _os.path.abspath(dir)
for seq in range(TMP_MAX):
# Try only a few names per directory.
for seq in range(100):
name = next(namer)
filename = _os.path.join(dir, name)
try:
@@ -213,8 +213,10 @@ def _get_default_tempdir(dirlist=None):
except FileExistsError:
pass
except PermissionError:
# See the comment in mkdtemp().
if _os.name == 'nt' and _os.path.isdir(dir):
# This exception is thrown when a directory with the chosen name
# already exists on windows.
if (_os.name == 'nt' and _os.path.isdir(dir) and
_os.access(dir, _os.W_OK)):
continue
break # no point trying more names in this directory
except OSError:
@@ -256,8 +258,10 @@ def _mkstemp_inner(dir, pre, suf, flags, output_type):
except FileExistsError:
continue # try again
except PermissionError:
# See the comment in mkdtemp().
if _os.name == 'nt' and _os.path.isdir(dir) and seq < TMP_MAX - 1:
# This exception is thrown when a directory with the chosen name
# already exists on windows.
if (_os.name == 'nt' and _os.path.isdir(dir) and
_os.access(dir, _os.W_OK)):
continue
else:
raise
@@ -382,14 +386,10 @@ def mkdtemp(suffix=None, prefix=None, dir=None):
except FileExistsError:
continue # try again
except PermissionError:
# On Posix, this exception is raised when the user has no
# write access to the parent directory.
# On Windows, it is also raised when a directory with
# the chosen name already exists, or if the parent directory
# is not a directory.
# We cannot distinguish between "directory-exists-error" and
# "access-denied-error".
if _os.name == 'nt' and _os.path.isdir(dir) and seq < TMP_MAX - 1:
# This exception is thrown when a directory with the chosen name
# already exists on windows.
if (_os.name == 'nt' and _os.path.isdir(dir) and
_os.access(dir, _os.W_OK)):
continue
else:
raise

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +0,0 @@
import sys
import types
import unittest
# bpo-46417: Test that structseq types used by the sys module are still
# valid when Py_Finalize()/Py_Initialize() are called multiple times.
class TestStructSeq(unittest.TestCase):
# test PyTypeObject members
def check_structseq(self, obj_type):
# ob_refcnt
self.assertGreaterEqual(sys.getrefcount(obj_type), 1)
# tp_base
self.assertIsSubclass(obj_type, tuple)
# tp_bases
self.assertEqual(obj_type.__bases__, (tuple,))
# tp_dict
self.assertIsInstance(obj_type.__dict__, types.MappingProxyType)
# tp_mro
self.assertEqual(obj_type.__mro__, (obj_type, tuple, object))
# tp_name
self.assertIsInstance(type.__name__, str)
# tp_subclasses
self.assertEqual(obj_type.__subclasses__(), [])
def test_sys_attrs(self):
for attr_name in (
'flags', # FlagsType
'float_info', # FloatInfoType
'hash_info', # Hash_InfoType
'int_info', # Int_InfoType
'thread_info', # ThreadInfoType
'version_info', # VersionInfoType
):
with self.subTest(attr=attr_name):
attr = getattr(sys, attr_name)
self.check_structseq(type(attr))
def test_sys_funcs(self):
func_names = ['get_asyncgen_hooks'] # AsyncGenHooksType
if hasattr(sys, 'getwindowsversion'):
func_names.append('getwindowsversion') # WindowsVersionType
for func_name in func_names:
with self.subTest(func=func_name):
func = getattr(sys, func_name)
obj = func()
self.check_structseq(type(obj))
try:
unittest.main(
module=(
'__main__'
if __name__ == '__main__'
# Avoiding a circular import:
else sys.modules['test._test_embed_structseq']
)
)
except SystemExit as exc:
if exc.args[0] != 0:
raise
print("Tests passed")

View File

@@ -1,681 +0,0 @@
"""This script contains the actual auditing tests.
It should not be imported directly, but should be run by the test_audit
module with arguments identifying each test.
"""
import contextlib
import os
import sys
class TestHook:
"""Used in standard hook tests to collect any logged events.
Should be used in a with block to ensure that it has no impact
after the test completes.
"""
def __init__(self, raise_on_events=None, exc_type=RuntimeError):
self.raise_on_events = raise_on_events or ()
self.exc_type = exc_type
self.seen = []
self.closed = False
def __enter__(self, *a):
sys.addaudithook(self)
return self
def __exit__(self, *a):
self.close()
def close(self):
self.closed = True
@property
def seen_events(self):
return [i[0] for i in self.seen]
def __call__(self, event, args):
if self.closed:
return
self.seen.append((event, args))
if event in self.raise_on_events:
raise self.exc_type("saw event " + event)
# Simple helpers, since we are not in unittest here
def assertEqual(x, y):
if x != y:
raise AssertionError(f"{x!r} should equal {y!r}")
def assertIn(el, series):
if el not in series:
raise AssertionError(f"{el!r} should be in {series!r}")
def assertNotIn(el, series):
if el in series:
raise AssertionError(f"{el!r} should not be in {series!r}")
def assertSequenceEqual(x, y):
if len(x) != len(y):
raise AssertionError(f"{x!r} should equal {y!r}")
if any(ix != iy for ix, iy in zip(x, y)):
raise AssertionError(f"{x!r} should equal {y!r}")
@contextlib.contextmanager
def assertRaises(ex_type):
try:
yield
assert False, f"expected {ex_type}"
except BaseException as ex:
if isinstance(ex, AssertionError):
raise
assert type(ex) is ex_type, f"{ex} should be {ex_type}"
def test_basic():
with TestHook() as hook:
sys.audit("test_event", 1, 2, 3)
assertEqual(hook.seen[0][0], "test_event")
assertEqual(hook.seen[0][1], (1, 2, 3))
def test_block_add_hook():
# Raising an exception should prevent a new hook from being added,
# but will not propagate out.
with TestHook(raise_on_events="sys.addaudithook") as hook1:
with TestHook() as hook2:
sys.audit("test_event")
assertIn("test_event", hook1.seen_events)
assertNotIn("test_event", hook2.seen_events)
def test_block_add_hook_baseexception():
# Raising BaseException will propagate out when adding a hook
with assertRaises(BaseException):
with TestHook(
raise_on_events="sys.addaudithook", exc_type=BaseException
) as hook1:
# Adding this next hook should raise BaseException
with TestHook() as hook2:
pass
def test_marshal():
import marshal
o = ("a", "b", "c", 1, 2, 3)
payload = marshal.dumps(o)
with TestHook() as hook:
assertEqual(o, marshal.loads(marshal.dumps(o)))
try:
with open("test-marshal.bin", "wb") as f:
marshal.dump(o, f)
with open("test-marshal.bin", "rb") as f:
assertEqual(o, marshal.load(f))
finally:
os.unlink("test-marshal.bin")
actual = [(a[0], a[1]) for e, a in hook.seen if e == "marshal.dumps"]
assertSequenceEqual(actual, [(o, marshal.version)] * 2)
actual = [a[0] for e, a in hook.seen if e == "marshal.loads"]
assertSequenceEqual(actual, [payload])
actual = [e for e, a in hook.seen if e == "marshal.load"]
assertSequenceEqual(actual, ["marshal.load"])
def test_pickle():
import pickle
class PicklePrint:
def __reduce_ex__(self, p):
return str, ("Pwned!",)
payload_1 = pickle.dumps(PicklePrint())
payload_2 = pickle.dumps(("a", "b", "c", 1, 2, 3))
# Before we add the hook, ensure our malicious pickle loads
assertEqual("Pwned!", pickle.loads(payload_1))
with TestHook(raise_on_events="pickle.find_class") as hook:
with assertRaises(RuntimeError):
# With the hook enabled, loading globals is not allowed
pickle.loads(payload_1)
# pickles with no globals are okay
pickle.loads(payload_2)
def test_monkeypatch():
class A:
pass
class B:
pass
class C(A):
pass
a = A()
with TestHook() as hook:
# Catch name changes
C.__name__ = "X"
# Catch type changes
C.__bases__ = (B,)
# Ensure bypassing __setattr__ is still caught
type.__dict__["__bases__"].__set__(C, (B,))
# Catch attribute replacement
C.__init__ = B.__init__
# Catch attribute addition
C.new_attr = 123
# Catch class changes
a.__class__ = B
actual = [(a[0], a[1]) for e, a in hook.seen if e == "object.__setattr__"]
assertSequenceEqual(
[(C, "__name__"), (C, "__bases__"), (C, "__bases__"), (a, "__class__")], actual
)
def test_open(testfn):
# SSLContext.load_dh_params uses Py_fopen() rather than normal open()
try:
import ssl
load_dh_params = ssl.create_default_context().load_dh_params
except ImportError:
load_dh_params = None
try:
import readline
except ImportError:
readline = None
def rl(name):
if readline:
return getattr(readline, name, None)
else:
return None
# Try a range of "open" functions.
# All of them should fail
with TestHook(raise_on_events={"open"}) as hook:
for fn, *args in [
(open, testfn, "r"),
(open, sys.executable, "rb"),
(open, 3, "wb"),
(open, testfn, "w", -1, None, None, None, False, lambda *a: 1),
(load_dh_params, testfn),
(rl("read_history_file"), testfn),
(rl("read_history_file"), None),
(rl("write_history_file"), testfn),
(rl("write_history_file"), None),
(rl("append_history_file"), 0, testfn),
(rl("append_history_file"), 0, None),
(rl("read_init_file"), testfn),
(rl("read_init_file"), None),
]:
if not fn:
continue
with assertRaises(RuntimeError):
try:
fn(*args)
except NotImplementedError:
if fn == load_dh_params:
# Not callable in some builds
load_dh_params = None
raise RuntimeError
else:
raise
actual_mode = [(a[0], a[1]) for e, a in hook.seen if e == "open" and a[1]]
actual_flag = [(a[0], a[2]) for e, a in hook.seen if e == "open" and not a[1]]
assertSequenceEqual(
[
i
for i in [
(testfn, "r"),
(sys.executable, "r"),
(3, "w"),
(testfn, "w"),
(testfn, "rb") if load_dh_params else None,
(testfn, "r") if readline else None,
("~/.history", "r") if readline else None,
(testfn, "w") if readline else None,
("~/.history", "w") if readline else None,
(testfn, "a") if rl("append_history_file") else None,
("~/.history", "a") if rl("append_history_file") else None,
(testfn, "r") if readline else None,
("<readline_init_file>", "r") if readline else None,
]
if i is not None
],
actual_mode,
)
assertSequenceEqual([], actual_flag)
def test_cantrace():
traced = []
def trace(frame, event, *args):
if frame.f_code == TestHook.__call__.__code__:
traced.append(event)
old = sys.settrace(trace)
try:
with TestHook() as hook:
# No traced call
eval("1")
# No traced call
hook.__cantrace__ = False
eval("2")
# One traced call
hook.__cantrace__ = True
eval("3")
# Two traced calls (writing to private member, eval)
hook.__cantrace__ = 1
eval("4")
# One traced call (writing to private member)
hook.__cantrace__ = 0
finally:
sys.settrace(old)
assertSequenceEqual(["call"] * 4, traced)
def test_mmap():
import mmap
with TestHook() as hook:
mmap.mmap(-1, 8)
assertEqual(hook.seen[0][1][:2], (-1, 8))
def test_ctypes_call_function():
import ctypes
import _ctypes
with TestHook() as hook:
_ctypes.call_function(ctypes._memmove_addr, (0, 0, 0))
assert ("ctypes.call_function", (ctypes._memmove_addr, (0, 0, 0))) in hook.seen, f"{ctypes._memmove_addr=} {hook.seen=}"
ctypes.CFUNCTYPE(ctypes.c_voidp)(ctypes._memset_addr)(1, 0, 0)
assert ("ctypes.call_function", (ctypes._memset_addr, (1, 0, 0))) in hook.seen, f"{ctypes._memset_addr=} {hook.seen=}"
with TestHook() as hook:
ctypes.cast(ctypes.c_voidp(0), ctypes.POINTER(ctypes.c_char))
assert "ctypes.call_function" in hook.seen_events
with TestHook() as hook:
ctypes.string_at(id("ctypes.string_at") + 40)
assert "ctypes.call_function" in hook.seen_events
assert "ctypes.string_at" in hook.seen_events
def test_posixsubprocess():
import multiprocessing.util
exe = b"xxx"
args = [b"yyy", b"zzz"]
with TestHook() as hook:
multiprocessing.util.spawnv_passfds(exe, args, ())
assert ("_posixsubprocess.fork_exec", ([exe], args, None)) in hook.seen
def test_excepthook():
def excepthook(exc_type, exc_value, exc_tb):
if exc_type is not RuntimeError:
sys.__excepthook__(exc_type, exc_value, exc_tb)
def hook(event, args):
if event == "sys.excepthook":
if not isinstance(args[2], args[1]):
raise TypeError(f"Expected isinstance({args[2]!r}, " f"{args[1]!r})")
if args[0] != excepthook:
raise ValueError(f"Expected {args[0]} == {excepthook}")
print(event, repr(args[2]))
sys.addaudithook(hook)
sys.excepthook = excepthook
raise RuntimeError("fatal-error")
def test_unraisablehook():
from _testcapi import err_formatunraisable
def unraisablehook(hookargs):
pass
def hook(event, args):
if event == "sys.unraisablehook":
if args[0] != unraisablehook:
raise ValueError(f"Expected {args[0]} == {unraisablehook}")
print(event, repr(args[1].exc_value), args[1].err_msg)
sys.addaudithook(hook)
sys.unraisablehook = unraisablehook
err_formatunraisable(RuntimeError("nonfatal-error"),
"Exception ignored for audit hook test")
def test_winreg():
from winreg import OpenKey, EnumKey, CloseKey, HKEY_LOCAL_MACHINE
def hook(event, args):
if not event.startswith("winreg."):
return
print(event, *args)
sys.addaudithook(hook)
k = OpenKey(HKEY_LOCAL_MACHINE, "Software")
EnumKey(k, 0)
try:
EnumKey(k, 10000)
except OSError:
pass
else:
raise RuntimeError("Expected EnumKey(HKLM, 10000) to fail")
kv = k.Detach()
CloseKey(kv)
def test_socket():
import socket
def hook(event, args):
if event.startswith("socket."):
print(event, *args)
sys.addaudithook(hook)
socket.gethostname()
# Don't care if this fails, we just want the audit message
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# Don't care if this fails, we just want the audit message
sock.bind(('127.0.0.1', 8080))
except Exception:
pass
finally:
sock.close()
def test_gc():
import gc
def hook(event, args):
if event.startswith("gc."):
print(event, *args)
sys.addaudithook(hook)
gc.get_objects(generation=1)
x = object()
y = [x]
gc.get_referrers(x)
gc.get_referents(y)
def test_http_client():
import http.client
def hook(event, args):
if event.startswith("http.client."):
print(event, *args[1:])
sys.addaudithook(hook)
conn = http.client.HTTPConnection('www.python.org')
try:
conn.request('GET', '/')
except OSError:
print('http.client.send', '[cannot send]')
finally:
conn.close()
def test_sqlite3():
import sqlite3
def hook(event, *args):
if event.startswith("sqlite3."):
print(event, *args)
sys.addaudithook(hook)
cx1 = sqlite3.connect(":memory:")
cx2 = sqlite3.Connection(":memory:")
# Configured without --enable-loadable-sqlite-extensions
try:
if hasattr(sqlite3.Connection, "enable_load_extension"):
cx1.enable_load_extension(False)
try:
cx1.load_extension("test")
except sqlite3.OperationalError:
pass
else:
raise RuntimeError("Expected sqlite3.load_extension to fail")
finally:
cx1.close()
cx2.close()
def test_sys_getframe():
import sys
def hook(event, args):
if event.startswith("sys."):
print(event, args[0].f_code.co_name)
sys.addaudithook(hook)
sys._getframe()
def test_sys_getframemodulename():
import sys
def hook(event, args):
if event.startswith("sys."):
print(event, *args)
sys.addaudithook(hook)
sys._getframemodulename()
def test_threading():
import _thread
def hook(event, args):
if event.startswith(("_thread.", "cpython.PyThreadState", "test.")):
print(event, args)
sys.addaudithook(hook)
lock = _thread.allocate_lock()
lock.acquire()
class test_func:
def __repr__(self): return "<test_func>"
def __call__(self):
sys.audit("test.test_func")
lock.release()
i = _thread.start_new_thread(test_func(), ())
lock.acquire()
handle = _thread.start_joinable_thread(test_func())
handle.join()
def test_threading_abort():
# Ensures that aborting PyThreadState_New raises the correct exception
import _thread
class ThreadNewAbortError(Exception):
pass
def hook(event, args):
if event == "cpython.PyThreadState_New":
raise ThreadNewAbortError()
sys.addaudithook(hook)
try:
_thread.start_new_thread(lambda: None, ())
except ThreadNewAbortError:
# Other exceptions are raised and the test will fail
pass
def test_wmi_exec_query():
import _wmi
def hook(event, args):
if event.startswith("_wmi."):
print(event, args[0])
sys.addaudithook(hook)
try:
_wmi.exec_query("SELECT * FROM Win32_OperatingSystem")
except WindowsError as e:
# gh-112278: WMI may be slow response when first called, but we still
# get the audit event, so just ignore the timeout
if e.winerror != 258:
raise
def test_syslog():
import syslog
def hook(event, args):
if event.startswith("syslog."):
print(event, *args)
sys.addaudithook(hook)
syslog.openlog('python')
syslog.syslog('test')
syslog.setlogmask(syslog.LOG_DEBUG)
syslog.closelog()
# implicit open
syslog.syslog('test2')
# open with default ident
syslog.openlog(logoption=syslog.LOG_NDELAY, facility=syslog.LOG_LOCAL0)
sys.argv = None
syslog.openlog()
syslog.closelog()
def test_not_in_gc():
import gc
hook = lambda *a: None
sys.addaudithook(hook)
for o in gc.get_objects():
if isinstance(o, list):
assert hook not in o
def test_time(mode):
import time
def hook(event, args):
if event.startswith("time."):
if mode == 'print':
print(event, *args)
elif mode == 'fail':
raise AssertionError('hook failed')
sys.addaudithook(hook)
time.sleep(0)
time.sleep(0.0625) # 1/16, a small exact float
try:
time.sleep(-1)
except ValueError:
pass
def test_sys_monitoring_register_callback():
import sys
def hook(event, args):
if event.startswith("sys.monitoring"):
print(event, args)
sys.addaudithook(hook)
sys.monitoring.register_callback(1, 1, None)
def test_winapi_createnamedpipe(pipe_name):
import _winapi
def hook(event, args):
if event == "_winapi.CreateNamedPipe":
print(event, args)
sys.addaudithook(hook)
_winapi.CreateNamedPipe(pipe_name, _winapi.PIPE_ACCESS_DUPLEX, 8, 2, 0, 0, 0, 0)
def test_assert_unicode():
import sys
sys.addaudithook(lambda *args: None)
try:
sys.audit(9)
except TypeError:
pass
else:
raise RuntimeError("Expected sys.audit(9) to fail.")
def test_sys_remote_exec():
import tempfile
pid = os.getpid()
event_pid = -1
event_script_path = ""
remote_event_script_path = ""
def hook(event, args):
if event not in ["sys.remote_exec", "cpython.remote_debugger_script"]:
return
print(event, args)
match event:
case "sys.remote_exec":
nonlocal event_pid, event_script_path
event_pid = args[0]
event_script_path = args[1]
case "cpython.remote_debugger_script":
nonlocal remote_event_script_path
remote_event_script_path = args[0]
sys.addaudithook(hook)
with tempfile.NamedTemporaryFile(mode='w+', delete=True) as tmp_file:
tmp_file.write("a = 1+1\n")
tmp_file.flush()
sys.remote_exec(pid, tmp_file.name)
assertEqual(event_pid, pid)
assertEqual(event_script_path, tmp_file.name)
assertEqual(remote_event_script_path, tmp_file.name)
if __name__ == "__main__":
from test.support import suppress_msvcrt_asserts
suppress_msvcrt_asserts()
test = sys.argv[1]
globals()[test](*sys.argv[2:])

View File

@@ -47,11 +47,7 @@ import _strptime
try:
import _pydatetime
except ImportError:
_pydatetime = None
try:
import _datetime
except ImportError:
_datetime = None
pass
#
pickle_loads = {pickle.loads, pickle._loads}
@@ -3015,6 +3011,7 @@ class TestDateTime(TestDate):
self.assertEqual(t.strftime("%z"), "-0200" + z)
self.assertEqual(t.strftime("%:z"), "-02:00:" + z)
@unittest.skip("TODO: RUSTPYTHON")
def test_strftime_special(self):
t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
s1 = t.strftime('%c')
@@ -3882,6 +3879,7 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
# gh-85432: The parameter was named "fmt" in the pure-Python impl.
t.strftime(format="%f")
@unittest.skip("TODO: RUSTPYTHON")
def test_strftime_special(self):
t = self.theclass(1, 2, 3, 4)
s1 = t.strftime('%I%p%Z')
@@ -4362,6 +4360,7 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase):
self.assertEqual(t.microsecond, 0)
self.assertIsNone(t.tzinfo)
@unittest.skip("TODO: RUSTPYTHON")
def test_zones(self):
est = FixedOffset(-300, "EST", 1)
utc = FixedOffset(0, "UTC", -2)

View File

@@ -40,7 +40,6 @@ BaseException
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ ├── PythonFinalizationError
│ └── RecursionError
├── StopAsyncIteration
├── StopIteration

View File

@@ -1,390 +0,0 @@
# Classes used for pickle testing.
# They are moved to separate file, so they can be loaded
# in other Python version for test_xpickle.
import sys
class C:
def __eq__(self, other):
return self.__dict__ == other.__dict__
# For test_load_classic_instance
class D(C):
def __init__(self, arg):
pass
class E(C):
def __getinitargs__(self):
return ()
import __main__
__main__.C = C
C.__module__ = "__main__"
__main__.D = D
D.__module__ = "__main__"
__main__.E = E
E.__module__ = "__main__"
# Simple mutable object.
class Object(object):
pass
# Hashable immutable key object containing unheshable mutable data.
class K:
def __init__(self, value):
self.value = value
def __reduce__(self):
# Shouldn't support the recursion itself
return K, (self.value,)
class WithSlots(object):
__slots__ = ('a', 'b')
class WithSlotsSubclass(WithSlots):
__slots__ = ('c',)
class WithSlotsAndDict(object):
__slots__ = ('a', '__dict__')
class WithPrivateAttrs(object):
def __init__(self, a):
self.__private = a
def get(self):
return self.__private
class WithPrivateAttrsSubclass(WithPrivateAttrs):
def __init__(self, a, b):
super().__init__(a)
self.__private = b
def get2(self):
return self.__private
class WithPrivateSlots(object):
__slots__ = ('__private',)
def __init__(self, a):
self.__private = a
def get(self):
return self.__private
class WithPrivateSlotsSubclass(WithPrivateSlots):
__slots__ = ('__private',)
def __init__(self, a, b):
super().__init__(a)
self.__private = b
def get2(self):
return self.__private
# For test_misc
class myint(int):
def __init__(self, x):
self.str = str(x)
# For test_misc and test_getinitargs
class initarg(C):
def __init__(self, a, b):
self.a = a
self.b = b
def __getinitargs__(self):
return self.a, self.b
# For test_metaclass
class metaclass(type):
pass
if sys.version_info >= (3,):
# Syntax not compatible with Python 2
exec('''
class use_metaclass(object, metaclass=metaclass):
pass
''')
else:
class use_metaclass(object):
__metaclass__ = metaclass
# Test classes for reduce_ex
class R:
def __init__(self, reduce=None):
self.reduce = reduce
def __reduce__(self, proto):
return self.reduce
class REX:
def __init__(self, reduce_ex=None):
self.reduce_ex = reduce_ex
def __reduce_ex__(self, proto):
return self.reduce_ex
class REX_one(object):
"""No __reduce_ex__ here, but inheriting it from object"""
_reduce_called = 0
def __reduce__(self):
self._reduce_called = 1
return REX_one, ()
class REX_two(object):
"""No __reduce__ here, but inheriting it from object"""
_proto = None
def __reduce_ex__(self, proto):
self._proto = proto
return REX_two, ()
class REX_three(object):
_proto = None
def __reduce_ex__(self, proto):
self._proto = proto
return REX_two, ()
def __reduce__(self):
raise AssertionError("This __reduce__ shouldn't be called")
class REX_four(object):
"""Calling base class method should succeed"""
_proto = None
def __reduce_ex__(self, proto):
self._proto = proto
return object.__reduce_ex__(self, proto)
class REX_five(object):
"""This one used to fail with infinite recursion"""
_reduce_called = 0
def __reduce__(self):
self._reduce_called = 1
return object.__reduce__(self)
class REX_six(object):
"""This class is used to check the 4th argument (list iterator) of
the reduce protocol.
"""
def __init__(self, items=None):
self.items = items if items is not None else []
def __eq__(self, other):
return type(self) is type(other) and self.items == other.items
def append(self, item):
self.items.append(item)
def __reduce__(self):
return type(self), (), None, iter(self.items), None
class REX_seven(object):
"""This class is used to check the 5th argument (dict iterator) of
the reduce protocol.
"""
def __init__(self, table=None):
self.table = table if table is not None else {}
def __eq__(self, other):
return type(self) is type(other) and self.table == other.table
def __setitem__(self, key, value):
self.table[key] = value
def __reduce__(self):
return type(self), (), None, None, iter(self.table.items())
class REX_state(object):
"""This class is used to check the 3th argument (state) of
the reduce protocol.
"""
def __init__(self, state=None):
self.state = state
def __eq__(self, other):
return type(self) is type(other) and self.state == other.state
def __setstate__(self, state):
self.state = state
def __reduce__(self):
return type(self), (), self.state
# For test_reduce_ex_None
class REX_None:
""" Setting __reduce_ex__ to None should fail """
__reduce_ex__ = None
# For test_reduce_None
class R_None:
""" Setting __reduce__ to None should fail """
__reduce__ = None
# For test_pickle_setstate_None
class C_None_setstate:
""" Setting __setstate__ to None should fail """
def __getstate__(self):
return 1
__setstate__ = None
# Test classes for newobj
# For test_newobj_generic and test_newobj_proxies
class MyInt(int):
sample = 1
if sys.version_info >= (3,):
class MyLong(int):
sample = 1
else:
class MyLong(long):
sample = long(1)
class MyFloat(float):
sample = 1.0
class MyComplex(complex):
sample = 1.0 + 0.0j
class MyStr(str):
sample = "hello"
if sys.version_info >= (3,):
class MyUnicode(str):
sample = "hello \u1234"
else:
class MyUnicode(unicode):
sample = unicode(r"hello \u1234", "raw-unicode-escape")
class MyTuple(tuple):
sample = (1, 2, 3)
class MyList(list):
sample = [1, 2, 3]
class MyDict(dict):
sample = {"a": 1, "b": 2}
class MySet(set):
sample = {"a", "b"}
class MyFrozenSet(frozenset):
sample = frozenset({"a", "b"})
myclasses = [MyInt, MyLong, MyFloat,
MyComplex,
MyStr, MyUnicode,
MyTuple, MyList, MyDict, MySet, MyFrozenSet]
# For test_newobj_overridden_new
class MyIntWithNew(int):
def __new__(cls, value):
raise AssertionError
class MyIntWithNew2(MyIntWithNew):
__new__ = int.__new__
# For test_newobj_list_slots
class SlotList(MyList):
__slots__ = ["foo"]
# Ruff "redefined while unused" false positive here due to `global` variables
# being assigned (and then restored) from within test methods earlier in the file
class SimpleNewObj(int): # noqa: F811
def __init__(self, *args, **kwargs):
# raise an error, to make sure this isn't called
raise TypeError("SimpleNewObj.__init__() didn't expect to get called")
def __eq__(self, other):
return int(self) == int(other) and self.__dict__ == other.__dict__
class ComplexNewObj(SimpleNewObj):
def __getnewargs__(self):
return ('%X' % self, 16)
class ComplexNewObjEx(SimpleNewObj):
def __getnewargs_ex__(self):
return ('%X' % self,), {'base': 16}
class ZeroCopyBytes(bytes):
readonly = True
c_contiguous = True
f_contiguous = True
zero_copy_reconstruct = True
def __reduce_ex__(self, protocol):
if protocol >= 5:
import pickle
return type(self)._reconstruct, (pickle.PickleBuffer(self),), None
else:
return type(self)._reconstruct, (bytes(self),)
def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, bytes(self))
__str__ = __repr__
@classmethod
def _reconstruct(cls, obj):
with memoryview(obj) as m:
obj = m.obj
if type(obj) is cls:
# Zero-copy
return obj
else:
return cls(obj)
class ZeroCopyBytearray(bytearray):
readonly = False
c_contiguous = True
f_contiguous = True
zero_copy_reconstruct = True
def __reduce_ex__(self, protocol):
if protocol >= 5:
import pickle
return type(self)._reconstruct, (pickle.PickleBuffer(self),), None
else:
return type(self)._reconstruct, (bytes(self),)
def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, bytes(self))
__str__ = __repr__
@classmethod
def _reconstruct(cls, obj):
with memoryview(obj) as m:
obj = m.obj
if type(obj) is cls:
# Zero-copy
return obj
else:
return cls(obj)
# For test_nested_names
class Nested:
class A:
class B:
class C:
pass
# For test_py_methods
class PyMethodsTest:
@staticmethod
def cheese():
return "cheese"
@classmethod
def wine(cls):
assert cls is PyMethodsTest
return "wine"
def biscuits(self):
assert isinstance(self, PyMethodsTest)
return "biscuits"
class Nested:
"Nested class"
@staticmethod
def ketchup():
return "ketchup"
@classmethod
def maple(cls):
assert cls is PyMethodsTest.Nested
return "maple"
def pie(self):
assert isinstance(self, PyMethodsTest.Nested)
return "pie"
# For test_c_methods
class Subclass(tuple):
class Nested(str):
pass

1136
Lib/test/pickletester.py vendored

File diff suppressed because it is too large Load Diff

BIN
Lib/test/pstats.pck vendored

Binary file not shown.

View File

@@ -531,7 +531,7 @@ xyzabc
(r'a[ ]*?\ (\d+).*', 'a 10', SUCCEED, 'found', 'a 10'),
(r'a[ ]*?\ (\d+).*', 'a 10', SUCCEED, 'found', 'a 10'),
# bug 127259: \Z shouldn't depend on multiline mode
(r'(?ms).*?x\s*\z(.*)','xx\nx\n', SUCCEED, 'g1', ''),
(r'(?ms).*?x\s*\Z(.*)','xx\nx\n', SUCCEED, 'g1', ''),
# bug 128899: uppercase literals under the ignorecase flag
(r'(?i)M+', 'MMM', SUCCEED, 'found', 'MMM'),
(r'(?i)m+', 'MMM', SUCCEED, 'found', 'MMM'),

33
Lib/test/seq_tests.py vendored
View File

@@ -261,20 +261,23 @@ class CommonTest(unittest.TestCase):
self.assertEqual(min(u), 0)
self.assertEqual(max(u), 2)
def test_add(self):
def test_addmul(self):
u1 = self.type2test([0])
u2 = self.type2test([0, 1])
self.assertEqual(u1, u1 + self.type2test())
self.assertEqual(u1, self.type2test() + u1)
self.assertEqual(u1 + self.type2test([1]), u2)
self.assertEqual(self.type2test([-1]) + u1, self.type2test([-1, 0]))
def test_mul(self):
u2 = self.type2test([0, 1])
self.assertEqual(self.type2test(), u2*0)
self.assertEqual(self.type2test(), 0*u2)
self.assertEqual(self.type2test(), u2*0)
self.assertEqual(self.type2test(), 0*u2)
self.assertEqual(u2, u2*1)
self.assertEqual(u2, 1*u2)
self.assertEqual(u2, u2*1)
self.assertEqual(u2, 1*u2)
self.assertEqual(u2+u2, u2*2)
self.assertEqual(u2+u2, 2*u2)
self.assertEqual(u2+u2, u2*2)
self.assertEqual(u2+u2, 2*u2)
self.assertEqual(u2+u2+u2, u2*3)
@@ -283,9 +286,8 @@ class CommonTest(unittest.TestCase):
class subclass(self.type2test):
pass
u3 = subclass([0, 1])
r = u3*1
self.assertEqual(r, u3)
self.assertIsNot(r, u3)
self.assertEqual(u3, u3*1)
self.assertIsNot(u3, u3*1)
def test_iadd(self):
u = self.type2test([0, 1])
@@ -346,21 +348,6 @@ class CommonTest(unittest.TestCase):
self.assertRaises(ValueError, a.__getitem__, slice(0, 10, 0))
self.assertRaises(TypeError, a.__getitem__, 'x')
def _assert_cmp(self, a, b, r):
self.assertIs(a == b, r == 0)
self.assertIs(a != b, r != 0)
self.assertIs(a > b, r > 0)
self.assertIs(a <= b, r <= 0)
self.assertIs(a < b, r < 0)
self.assertIs(a >= b, r >= 0)
def test_cmp(self):
a = self.type2test([0, 1])
self._assert_cmp(a, a, 0)
self._assert_cmp(a, self.type2test([0, 1]), 0)
self._assert_cmp(a, self.type2test([0]), 1)
self._assert_cmp(a, self.type2test([0, 2]), -1)
def test_count(self):
a = self.type2test([0, 1, 2])*3
self.assertEqual(a.count(0), 3)
@@ -439,7 +426,7 @@ class CommonTest(unittest.TestCase):
self.assertEqual(lst2, lst)
self.assertNotEqual(id(lst2), id(lst))
@unittest.skip("TODO: RUSTPYTHON; hangs")
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_free_after_iterating(self):
support.check_free_after_iterating(self, iter, self.type2test)
support.check_free_after_iterating(self, reversed, self.type2test)

View File

@@ -90,55 +90,6 @@ class BaseTest:
args = self.fixtype(args)
getattr(obj, methodname)(*args)
def _get_teststrings(self, charset, digits):
base = len(charset)
teststrings = set()
for i in range(base ** digits):
entry = []
for j in range(digits):
i, m = divmod(i, base)
entry.append(charset[m])
teststrings.add(''.join(entry))
teststrings = [self.fixtype(ts) for ts in teststrings]
return teststrings
def test_add(self):
s = self.fixtype('ab')
self.assertEqual(s + self.fixtype(''), s)
self.assertEqual(self.fixtype('') + s, s)
self.assertEqual(s + self.fixtype('cd'), self.fixtype('abcd'))
def test_mul(self):
s = self.fixtype('ab')
self.assertEqual(s*0, self.fixtype(''))
self.assertEqual(0*s, self.fixtype(''))
self.assertEqual(s*1, s)
self.assertEqual(1*s, s)
self.assertEqual(s*2, self.fixtype('abab'))
self.assertEqual(2*s, self.fixtype('abab'))
class subclass(self.type2test):
pass
s = subclass(self.fixtype('ab'))
r = s*1
self.assertEqual(r, s)
self.assertIsNot(r, s)
def _assert_cmp(self, a, b, r):
self.assertIs(a == b, r == 0)
self.assertIs(a != b, r != 0)
self.assertIs(a > b, r > 0)
self.assertIs(a <= b, r <= 0)
self.assertIs(a < b, r < 0)
self.assertIs(a >= b, r >= 0)
def test_cmp(self):
a = self.fixtype('ab')
self._assert_cmp(a, a, 0)
self._assert_cmp(a, self.fixtype('ab'), 0)
self._assert_cmp(a, self.fixtype('a'), 1)
self._assert_cmp(a, self.fixtype('ac'), -1)
def test_count(self):
self.checkequal(3, 'aaa', 'count', 'a')
self.checkequal(0, 'aaa', 'count', 'b')
@@ -179,7 +130,17 @@ class BaseTest:
# For a variety of combinations,
# verify that str.count() matches an equivalent function
# replacing all occurrences and then differencing the string lengths
teststrings = self._get_teststrings(['', 'a', 'b'], 7)
charset = ['', 'a', 'b']
digits = 7
base = len(charset)
teststrings = set()
for i in range(base ** digits):
entry = []
for j in range(digits):
i, m = divmod(i, base)
entry.append(charset[m])
teststrings.add(''.join(entry))
teststrings = [self.fixtype(ts) for ts in teststrings]
for i in teststrings:
n = len(i)
for j in teststrings:
@@ -236,7 +197,17 @@ class BaseTest:
# For a variety of combinations,
# verify that str.find() matches __contains__
# and that the found substring is really at that location
teststrings = self._get_teststrings(['', 'a', 'b', 'c'], 5)
charset = ['', 'a', 'b', 'c']
digits = 5
base = len(charset)
teststrings = set()
for i in range(base ** digits):
entry = []
for j in range(digits):
i, m = divmod(i, base)
entry.append(charset[m])
teststrings.add(''.join(entry))
teststrings = [self.fixtype(ts) for ts in teststrings]
for i in teststrings:
for j in teststrings:
loc = i.find(j)
@@ -273,7 +244,17 @@ class BaseTest:
# For a variety of combinations,
# verify that str.rfind() matches __contains__
# and that the found substring is really at that location
teststrings = self._get_teststrings(['', 'a', 'b', 'c'], 5)
charset = ['', 'a', 'b', 'c']
digits = 5
base = len(charset)
teststrings = set()
for i in range(base ** digits):
entry = []
for j in range(digits):
i, m = divmod(i, base)
entry.append(charset[m])
teststrings.add(''.join(entry))
teststrings = [self.fixtype(ts) for ts in teststrings]
for i in teststrings:
for j in teststrings:
loc = i.rfind(j)
@@ -314,19 +295,6 @@ class BaseTest:
else:
self.checkraises(TypeError, 'hello', 'index', 42)
# For a variety of combinations,
# verify that str.index() matches __contains__
# and that the found substring is really at that location
teststrings = self._get_teststrings(['', 'a', 'b', 'c'], 5)
for i in teststrings:
for j in teststrings:
if j in i:
loc = i.index(j)
self.assertGreaterEqual(loc, 0)
self.assertEqual(i[loc:loc+len(j)], j)
else:
self.assertRaises(ValueError, i.index, j)
def test_rindex(self):
self.checkequal(12, 'abcdefghiabc', 'rindex', '')
self.checkequal(3, 'abcdefghiabc', 'rindex', 'def')
@@ -353,19 +321,6 @@ class BaseTest:
else:
self.checkraises(TypeError, 'hello', 'rindex', 42)
# For a variety of combinations,
# verify that str.rindex() matches __contains__
# and that the found substring is really at that location
teststrings = self._get_teststrings(['', 'a', 'b', 'c'], 5)
for i in teststrings:
for j in teststrings:
if j in i:
loc = i.rindex(j)
self.assertGreaterEqual(loc, 0)
self.assertEqual(i[loc:loc+len(j)], j)
else:
self.assertRaises(ValueError, i.rindex, j)
def test_find_periodic_pattern(self):
"""Cover the special path for periodic patterns."""
def reference_find(p, s):
@@ -482,10 +437,8 @@ class BaseTest:
self.checkequal(' a\n b', ' \ta\n\tb', 'expandtabs', 1)
self.checkraises(TypeError, 'hello', 'expandtabs', 42, 42)
# TODO: RUSTPYTHON; expandtabs overflow checks
# if sys.maxsize < (1 << 32) and struct.calcsize('P') == 4:
#
# This test is only valid when sizeof(int) == sizeof(void*) == 4.
# XXX RUSTPYTHON TODO: expandtabs overflow checks
if sys.maxsize < (1 << 32) and struct.calcsize('P') == 4 and False:
self.checkraises(OverflowError,
'\ta\n\tb', 'expandtabs', sys.maxsize)
@@ -815,15 +768,6 @@ class BaseTest:
self.checkraises(TypeError, 'hello', 'replace', 42, 'h')
self.checkraises(TypeError, 'hello', 'replace', 'h', 42)
def test_replacement_on_buffer_boundary(self):
# gh-127971: Check we don't read past the end of the buffer when a
# potential match misses on the last character.
any_3_nonblank_codepoints = '!!!'
seven_codepoints = any_3_nonblank_codepoints + ' ' + any_3_nonblank_codepoints
a = (' ' * 243) + seven_codepoints + (' ' * 7)
b = ' ' * 6 + chr(256)
a.replace(seven_codepoints, b)
def test_replace_uses_two_way_maxcount(self):
# Test that maxcount works in _two_way_count in fastsearch.h
A, B = "A"*1000, "B"*1000
@@ -836,7 +780,7 @@ class BaseTest:
self.checkequal(AABAA + "ccc",
AABAA + ABBA, 'replace', ABBA, "ccc", 2)
@unittest.skip("TODO: RUSTPYTHON; may only apply to 32-bit platforms")
@unittest.skip("TODO: RUSTPYTHON, may only apply to 32-bit platforms")
@unittest.skipIf(sys.maxsize > (1 << 32) or struct.calcsize('P') != 4,
'only applies to 32-bit platforms')
def test_replace_overflow(self):
@@ -1190,8 +1134,8 @@ class StringLikeTest(BaseTest):
self.checkequal('\u2160\u2171\u2172',
'\u2170\u2171\u2172', 'capitalize')
# check with Ll chars with no upper - nothing changes here
self.checkequal('\u1d00\u1d86\u0221\u1fb7',
'\u1d00\u1d86\u0221\u1fb7', 'capitalize')
self.checkequal('\u019b\u1d00\u1d86\u0221\u1fb7',
'\u019b\u1d00\u1d86\u0221\u1fb7', 'capitalize')
def test_startswith(self):
self.checkequal(True, 'hello', 'startswith', 'he')
@@ -1304,7 +1248,9 @@ class StringLikeTest(BaseTest):
self.checkequal(False, 'asd', '__contains__', 'asdf')
self.checkequal(False, '', '__contains__', 'asdf')
@unittest.expectedFailure # TODO: RUSTPYTHON
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_subscript(self):
self.checkequal('a', 'abc', '__getitem__', 0)
self.checkequal('c', 'abc', '__getitem__', -1)
@@ -1346,7 +1292,6 @@ class StringLikeTest(BaseTest):
slice(start, stop, step))
def test_mul(self):
super().test_mul()
self.checkequal('', 'abc', '__mul__', -1)
self.checkequal('', 'abc', '__mul__', 0)
self.checkequal('abc', 'abc', '__mul__', 1)
@@ -1559,7 +1504,8 @@ class StringLikeTest(BaseTest):
self.checkequal(True, s, 'startswith', 'h', None, -2)
self.checkequal(False, s, 'startswith', 'x', None, None)
@unittest.expectedFailure # TODO: RUSTPYTHON
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_find_etc_raise_correct_error_messages(self):
# issue 11828
s = 'hello'

View File

@@ -548,6 +548,7 @@ def requires_lzma(reason='requires lzma'):
import lzma
except ImportError:
lzma = None
lzma = None # XXX: RUSTPYTHON; xz is not supported yet
return unittest.skipUnless(lzma, reason)
def requires_zstd(reason='requires zstd'):
@@ -855,6 +856,8 @@ def gc_collect():
longer than expected. This function tries its best to force all garbage
objects to disappear.
"""
return # TODO: RUSTPYTHON
import gc
gc.collect()
gc.collect()
@@ -862,6 +865,13 @@ def gc_collect():
@contextlib.contextmanager
def disable_gc():
# TODO: RUSTPYTHON; GC is not supported yet
try:
yield
finally:
pass
return
import gc
have_gc = gc.isenabled()
gc.disable()
@@ -1994,6 +2004,10 @@ def _check_tracemalloc():
def check_free_after_iterating(test, iter, cls, args=()):
# TODO: RUSTPYTHON; GC is not supported yet
test.assertTrue(False)
return
done = False
def wrapper():
class A(cls):
@@ -3034,10 +3048,6 @@ def get_signal_name(exitcode):
except KeyError:
pass
# Format Windows exit status as hexadecimal
if 0xC0000000 <= exitcode:
return f"0x{exitcode:X}"
return None
class BrokenIter:

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