Merge remote-tracking branch 'origin/main' into copilot/fix-isalnum-function-behavior

# Conflicts:
#	crates/sre_engine/src/string.rs
#	extra_tests/snippets/builtin_str_unicode.py
This commit is contained in:
copilot-swe-agent[bot] authored and GitHub committed 2026-08-02 07:23:30 +00:00
commit 9bc27415e4
923 files changed
+193824 -22452

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
---
name: apple-container
description: Run RustPython tests in Apple's `container` CLI Linux environment. Use when user asks to run or compare Linux test results on macOS.
allowed-tools: Bash(container:*) Bash(cargo:*) Read Grep Glob
---
# Run Tests in Linux Container (Apple `container` CLI)
Run RustPython tests inside a Linux container using Apple's `container` CLI.
NEVER use Docker, Podman, or any other container runtime. Only use the `container` command.
## Arguments
- Test command to run (examples: `test_io`, `test_codecs -v`, `test_io -v -m "test_errors"`)
## Prerequisites
- The `container` CLI is installed via `brew install container`.
- The dev image `rustpython-dev` is already built.
## Workflow
1. Check whether the test container is already running:
```shell
container list 2>/dev/null | grep rustpython-test
```
2. Start the container if it is not running:
```shell
container run -d --name rustpython-test -m 8G -c 4 \
--mount type=bind,source="$(pwd)",target=/workspace \
-w /workspace rustpython-dev sleep infinity
```
3. Run the requested test command inside the container:
```shell
container exec rustpython-test cargo run --release -- -m test <test-args>
```
4. Report results:
- Show pass/fail summary and expected failures / unexpected successes.
- Highlight new failures compared to macOS results, if available.
- Do not stop or remove the container after testing.
## Notes
- The workspace is bind-mounted, so local code changes are immediately available in the container.
- Use `container exec rustpython-test sh -c "..."` for any command inside the container.
- Rebuild after code changes with:
```shell
container exec rustpython-test sh -c "cargo build --release"
```
- Stop the container when explicitly requested:
```shell
container rm -f rustpython-test
```
@@ -0,0 +1,53 @@
---
name: investigate-test-failure
description: Investigate a failing RustPython test, compare with CPython behavior, and decide whether to implement a fix or prepare incompatibility issue details.
allowed-tools: Bash(python3:*) Bash(cargo run:*) Bash(gh issue create:*) Read Grep Glob Bash(git add:*) Bash(git commit:*) Bash(cargo fmt:*) Bash(git diff:*) Task
---
# Investigate Test Failure
Investigate why a specific test is failing and determine whether it can be fixed now or should be reported as an incompatibility issue.
## Arguments
- Failed test identifier (example: `test_inspect.TestGetSourceBase.test_getsource_reload`)
## Workflow
1. Analyze the failure cause:
- Read the test code.
- Analyze failure message and traceback.
- Check related RustPython implementation code.
2. Verify behavior in CPython:
- Run the test with `python3 -m unittest`.
- Document expected behavior/output.
3. Determine fix feasibility:
- Simple fix (import issues, small logic bugs): implement fix, run formatting and checks, review changes, and commit.
- Complex fix (major missing feature): gather issue information and report to user.
Pre-commit review process:
- Run `git diff` to review local changes.
- Use a focused subagent review to compare implementation against CPython behavior and check for missed edge cases.
- Commit only after review passes.
4. For complex issues, collect issue information using `.github/ISSUE_TEMPLATE/report-incompatibility.md`:
- Feature: missing or broken Python feature.
- Minimal reproduction code.
- CPython behavior (`python3`).
- RustPython behavior (`cargo run`).
- Python documentation reference link.
Report the collected information to the user. Create an issue only when explicitly requested.
Example issue command:
```shell
gh issue create --template report-incompatibility.md --title "..." --body "..."
```
@@ -0,0 +1,60 @@
---
name: rustpython-capi-expansion
description: Implement missing RustPython C-API functions in crates/capi using the pyo3-ffi header split mapping (`pyo3-ffi/src/*.rs`, mirroring CPython C API headers). Use this whenever the user asks to add or port C-API functions (for example from setobject.h, dictobject.h, unicodeobject.h) or add capi tests.
---
# RustPython C-API Expansion
Use this workflow for adding missing C-API functions to RustPython.
## Source of truth for target files
- Use this mapping source: `pyo3-ffi/src/*.rs`, which mirrors the CPython header split used by the C API.
- Map requested header APIs to `crates/capi/src/<header_basename>.rs` using that split. Examples:
- `setobject.h` -> `crates/capi/src/setobject.rs`
- `dictobject.h` -> `crates/capi/src/dictobject.rs`
- `unicodeobject.h` -> `crates/capi/src/unicodeobject.rs`
- Do not invent alternate target modules when the header split implies a direct target.
- If the target file is not present yet, create it and wire it in `crates/capi/src/lib.rs`.
## Implementation workflow
1. Identify requested missing APIs from the user request and their originating C API header.
2. Open nearby capi modules in `crates/capi/src/` and follow existing style and patterns.
3. Implement only the requested functions in the mapped target file.
4. Keep behavior aligned with CPython C-API contracts.
5. Prefer using existing `rustpython-vm` functionality as much as possible instead of re-implementing behavior in capi.
6. If a needed `rustpython-vm` helper exists but is private, make it public with a minimal, focused visibility change.
7. Prefer direct contract assumptions over defensive null checks unless required by the established local style.
8. Add basic tests only; do not overfit with very specific edge-case clutter.
9. In tests, use `pyo3` only as a safe wrapper over the API. Avoid raw pointer-heavy direct FFI-style tests.
10. Run tests from `crates/capi`.
## Testing rules
- Run test commands with working directory set to `crates/capi`.
- Prefer targeted tests first (module/function filter), then broader capi tests if needed.
- Keep test names concise (no required `test_` prefix).
## Style rules
- Follow existing RustPython capi coding style in neighboring files.
- Reuse `rustpython-vm` methods and types first; avoid duplicating VM logic in capi wrappers.
- When exposing previously private VM helpers, keep the API surface minimal and avoid unrelated refactors.
- Only expose and implement ABI-stable C-API surface needed for `abi3` / `abi3t`.
- Add comments only when they explain non-obvious behavior.
- Keep edits minimal and focused on requested API expansion.
## Completion checklist
- [ ] All requested functions implemented in mapped target file.
- [ ] New module exported in `crates/capi/src/lib.rs` when applicable.
- [ ] Basic safe-wrapper `pyo3` tests added/updated.
- [ ] Tests executed from `crates/capi` and passing for changed area.
- [ ] Final response includes changed file paths and test command summary.
## Example prompts this skill should handle
- "Implement these missing functions from `dictobject.h`."
- "Add `setobject.h` C-API functions in RustPython and include basic tests."
- "Port the listed `unicodeobject.h` APIs in capi, follow existing style, and run tests from `crates/capi`."
@@ -0,0 +1,34 @@
---
name: upgrade-pylib-next
description: Pick the next CPython stdlib module ready for upgrade based on update_lib todo output and skip modules with open upgrade PRs.
allowed-tools: Skill(upgrade-pylib) Bash(gh pr list:*) Bash(cargo run:*)
---
# Upgrade Next Python Library
Find the next Python library module ready for upgrade and then run the `upgrade-pylib` workflow for it.
## Workflow
1. Get current TODO status:
```shell
cargo run --release -- scripts/update_lib todo 2>/dev/null
```
2. Get open upgrade PRs:
```shell
gh pr list --search "Update in:title" --json number,title --template '{{range .}}#{{.number}} {{.title}}{{"\\n"}}{{end}}'
```
3. From TODO output, select modules in this priority order:
- `[ ] [no deps]`
- `[ ] [0/n]` where dependencies are all upgraded (for example `[0/3]`, `[0/5]`)
4. Skip modules that already have an open upgrade PR.
5. Run upgrade for selected module using the `upgrade-pylib` workflow.
If no modules match, report that eligible modules are blocked by dependencies.
+102
View File
@@ -0,0 +1,102 @@
---
name: upgrade-pylib
description: Upgrade a Python standard library module from CPython into RustPython using scripts/update_lib and then triage and mark remaining failures.
allowed-tools: Bash(git add:*) Bash(git commit:*) Bash(git diff:*) Bash(cargo run:*) Bash(python3 scripts/update_lib quick:*) Bash(python3 scripts/update_lib auto-mark:*)
---
# Upgrade Python Library from CPython
Upgrade a Python standard library module from CPython to RustPython.
## Arguments
- Library name or path (examples: `inspect`, `asyncio`, `json`, `cpython/Lib/inspect.py`, `cpython/Lib/json/`)
## Important: Report Tool Issues First
If `scripts/update_lib` shows a bug, missing automation, or unexpected behavior, stop the upgrade and report the tooling issue first, including:
1. What you were trying to do:
- Library name.
- Full command used.
2. What went wrong or what is missing.
3. Expected vs actual behavior.
## Workflow
1. Run quick upgrade with update_lib:
- `python3 scripts/update_lib quick <module>`
- or `python3 scripts/update_lib quick cpython/Lib/<module>.py`
- or `python3 scripts/update_lib quick cpython/Lib/<module>/`
This step copies library files, patches tests while preserving markers where possible, runs tests, auto-marks new failures, removes stale `@unittest.expectedFailure`, and creates a commit.
If warnings like `WARNING: TestCFoo does not exist in remote file` appear, class structure changed and some markers must be restored manually.
2. Review diff and restore RustPython-specific changes:
- Run `git diff Lib/test/test_<module>`.
- Restore only changes with explicit `RUSTPYTHON` comments.
- Do not restore unrelated upstream CPython changes.
3. Investigate failing dependent tests:
- Get dependencies:
```shell
cargo run --release -- scripts/update_lib deps <module>
```
- Find direct dependent test modules from the `- [ ] <module>:` line.
- Run dependent tests and collect failures:
```shell
cargo run --release -- -m test <test_modules...> 2>&1 | grep -E "^(FAIL|ERROR):"
```
- For each failing test identifier, investigate with the `investigate-test-failure` workflow.
4. Mark remaining failures with auto-mark:
- `python3 scripts/update_lib auto-mark Lib/test/test_<module>.py --mark-failure`
- or `python3 scripts/update_lib auto-mark Lib/test/test_<module>/ --mark-failure`
Note: `--mark-failure` marks all current failures including regressions; review carefully.
5. Handle panics manually:
- For tests that panic/crash/hang, use `@unittest.skip("TODO: RUSTPYTHON; ...")` rather than expectedFailure.
6. Handle class-specific failures:
- If only one subclass fails (for example `TestCFoo` but not `TestPyFoo`), move marker to the failing subclass override.
7. Commit test-fix markers as a separate commit:
```shell
git add -u && git commit -m "Mark failing tests"
```
## Example usage
```text
upgrade-pylib inspect
upgrade-pylib json
upgrade-pylib asyncio
upgrade-pylib cpython/Lib/inspect.py
upgrade-pylib cpython/Lib/json/
```
## Notes
- The `cpython/` directory should contain the CPython source used for sync.
- `scripts/update_lib` modes:
- `quick`: patch + auto-mark
- `migrate`: patch only
- `auto-mark`: mark failures only
- `copy-lib`: copy library files only
- Patching transfers `@unittest.expectedFailure` and `@unittest.skip` with `TODO: RUSTPYTHON` markers where possible.
- The tool does not preserve every RustPython-specific change; always review and restore explicit RUSTPYTHON-marked logic.
+13
View File
@@ -6,3 +6,16 @@ rustflags = "-C link-args=-Wl,--stack,8000000"
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=getrandom_backend=\"wasm_js\""]
# Enforce a 1 MB stack limit for WASI targets.
# (Runaway heap growth is fixed by the smaller DataStack chunk
# and the size‑optimized wasm‑release profile.)
[target.wasm32-wasip1]
rustflags = [
"-C", "link-arg=-zstack-size=1048576",
]
[target.wasm32-wasip2]
rustflags = [
"-C", "link-arg=-zstack-size=1048576",
]
-46
View File
@@ -1,46 +0,0 @@
---
allowed-tools: Bash(container *), Bash(cargo *), Read, Grep, Glob
---
# Run Tests in Linux Container (Apple `container` CLI)
Run RustPython tests inside a Linux container using Apple's `container` CLI.
**NEVER use Docker, Podman, or any other container runtime.** Only use the `container` command.
## Arguments
- `$ARGUMENTS`: Test command to run (e.g., `test_io`, `test_codecs -v`, `test_io -v -m "test_errors"`)
## Prerequisites
The `container` CLI is installed via `brew install container`.
The dev image `rustpython-dev` is already built.
## Steps
1. **Check if the container is already running**
```shell
container list 2>/dev/null | grep rustpython-test
```
2. **Start the container if not running**
```shell
container run -d --name rustpython-test -m 8G -c 4 \
--mount type=bind,source=.,target=/workspace \
-w /workspace rustpython-dev sleep infinity
```
3. **Run the test inside the container**
```shell
container exec rustpython-test sh -c "cargo run --release -- -m test $ARGUMENTS"
```
4. **Report results**
- Show test summary (pass/fail counts, expected failures, unexpected successes)
- Highlight any new failures compared to macOS results if available
- Do NOT stop or remove the container after testing (keep it for reuse)
## Notes
- The workspace is bind-mounted, so local code changes are immediately available
- Use `container exec rustpython-test sh -c "..."` for any command inside the container
- To rebuild after code changes, run: `container exec rustpython-test sh -c "cargo build --release"`
- To stop the container when done: `container rm -f rustpython-test`
@@ -1,49 +0,0 @@
---
allowed-tools: Bash(python3:*), Bash(cargo run:*), Read, Grep, Glob, Bash(git add:*), Bash(git commit:*), Bash(cargo fmt:*), Bash(git diff:*), Task
---
# Investigate Test Failure
Investigate why a specific test is failing and determine if it can be fixed or needs an issue.
## Arguments
- `$ARGUMENTS`: Failed test identifier (e.g., `test_inspect.TestGetSourceBase.test_getsource_reload`)
## Steps
1. **Analyze failure cause**
- Read the test code
- Analyze failure message/traceback
- Check related RustPython code
2. **Verify behavior in CPython**
- Run the test with `python3 -m unittest` to confirm expected behavior
- Document the expected output
3. **Determine fix feasibility**
- **Simple fix** (import issues, small logic bugs): Fix code → Run `cargo fmt --all` → Pre-commit review → Commit
- **Complex fix** (major unimplemented features): Collect issue info and report to user
**Pre-commit review process**:
- Run `git diff` to see the changes
- Use Task tool with `general-purpose` subagent to review:
- Compare implementation against cpython/ source code
- Verify the fix aligns with CPython behavior
- Check for any missed edge cases
- Proceed to commit only after review passes
4. **For complex issues - Collect issue information**
Following `.github/ISSUE_TEMPLATE/report-incompatibility.md` format:
- **Feature**: Description of missing/broken Python feature
- **Minimal reproduction code**: Smallest code that reproduces the issue
- **CPython behavior**: Result when running with python3
- **RustPython behavior**: Result when running with cargo run
- **Python Documentation link**: Link to relevant CPython docs
Report collected information to the user. Issue creation is done only upon user request.
Example issue creation command:
```
gh issue create --template report-incompatibility.md --title "..." --body "..."
```
-33
View File
@@ -1,33 +0,0 @@
---
allowed-tools: Skill(upgrade-pylib), Bash(gh pr list:*)
---
# Upgrade Next Python Library
Find the next Python library module ready for upgrade and run `/upgrade-pylib` for it.
## Current TODO Status
!`cargo run --release -- scripts/update_lib todo 2>/dev/null`
## Open Upgrade PRs
!`gh pr list --search "Update in:title" --json number,title --template '{{range .}}#{{.number}} {{.title}}{{"\n"}}{{end}}'`
## Instructions
From the TODO list above, find modules matching these patterns (in priority order):
1. `[ ] [no deps]` - Modules with no dependencies (can be upgraded immediately)
2. `[ ] [0/n]` - Modules where all dependencies are already upgraded (e.g., `[0/3]`, `[0/5]`)
These patterns indicate modules that are ready to upgrade without blocking dependencies.
**Important**: Skip any modules that already have an open PR in the "Open Upgrade PRs" list above.
**After identifying a suitable module**, run:
```
/upgrade-pylib <module_name>
```
If no modules match these criteria, inform the user that all eligible modules have dependencies that need to be upgraded first.
-157
View File
@@ -1,157 +0,0 @@
---
allowed-tools: Bash(git add:*), Bash(git commit:*), Bash(python3 scripts/update_lib quick:*), Bash(python3 scripts/update_lib auto-mark:*)
---
# Upgrade Python Library from CPython
Upgrade a Python standard library module from CPython to RustPython.
## Arguments
- `$ARGUMENTS`: Library name to upgrade (e.g., `inspect`, `asyncio`, `json`)
## Important: Report Tool Issues First
If during the upgrade process you encounter any of the following issues with `scripts/update_lib`:
- A feature that should be automated but isn't supported
- A bug or unexpected behavior in the tool
- Missing functionality that would make the upgrade easier
**STOP the upgrade and report the issue first.** Describe:
1. What you were trying to do
- Library name
- The full command executed (e.g. python scripts/update_lib quick cpython/Lib/$ARGUMENTS.py)
2. What went wrong or what's missing
3. Expected vs actual behavior
This helps improve the tooling for future upgrades.
## Steps
1. **Run quick upgrade with update_lib**
- Run: `python3 scripts/update_lib quick $ARGUMENTS` (module name)
- Or: `python3 scripts/update_lib quick cpython/Lib/$ARGUMENTS.py` (library file path)
- Or: `python3 scripts/update_lib quick cpython/Lib/$ARGUMENTS/` (library directory path)
- This will:
- Copy library files (delete existing `Lib/$ARGUMENTS.py` or `Lib/$ARGUMENTS/`, then copy from `cpython/Lib/`)
- Patch test files preserving existing RustPython markers
- Run tests and auto-mark new test failures (not regressions)
- Remove `@unittest.expectedFailure` from tests that now pass
- Create a git commit with the changes
- **Handle warnings**: If you see warnings like `WARNING: TestCFoo does not exist in remote file`, it means the class structure changed and markers couldn't be transferred automatically. These need to be manually restored in step 2 or added in step 3.
2. **Review git diff and restore RUSTPYTHON-specific changes**
- Run `git diff Lib/test/test_$ARGUMENTS` to review all changes
- **Only restore changes that have explicit `RUSTPYTHON` comments**. Look for:
- `# XXX: RUSTPYTHON` or `# XXX RUSTPYTHON` - Comments marking RustPython-specific code modifications
- `# TODO: RUSTPYTHON` - Comments marking tests that need work
- Code changes with inline `# ... RUSTPYTHON` comments
- **Do NOT restore other diff changes** - these are likely upstream CPython changes, not RustPython-specific modifications
- When restoring, preserve the original context and formatting
3. **Investigate test failures with subagent**
- First, get dependent tests using the deps command:
```
cargo run --release -- scripts/update_lib deps $ARGUMENTS
```
- Look for the line `- [ ] $ARGUMENTS: test_xxx test_yyy ...` to get the direct dependent tests
- Run those tests to collect failures:
```
cargo run --release -- -m test test_xxx test_yyy ... 2>&1 | grep -E "^(FAIL|ERROR):"
```
- For example, if deps output shows `- [ ] linecache: test_bdb test_inspect test_linecache test_traceback test_zipimport`, run:
```
cargo run --release -- -m test test_bdb test_inspect test_linecache test_traceback test_zipimport 2>&1 | grep -E "^(FAIL|ERROR):"
```
- For each failure, use the Task tool with `general-purpose` subagent to investigate:
- Subagent should follow the `/investigate-test-failure` skill workflow
- Pass the failed test identifier as the argument (e.g., `test_inspect.TestGetSourceBase.test_getsource_reload`)
- If subagent can fix the issue easily: fix and commit
- If complex issue: subagent collects issue info and reports back (issue creation on user request only)
- Using subagent prevents context pollution in the main conversation
4. **Mark remaining test failures with auto-mark**
- Run: `python3 scripts/update_lib auto-mark Lib/test/test_$ARGUMENTS.py --mark-failure`
- Or for directory: `python3 scripts/update_lib auto-mark Lib/test/test_$ARGUMENTS/ --mark-failure`
- This will:
- Run tests and mark ALL failing tests with `@unittest.expectedFailure`
- Remove `@unittest.expectedFailure` from tests that now pass
- **Note**: The `--mark-failure` flag marks all failures including regressions. Review the changes before committing.
5. **Handle panics manually**
- If any tests cause panics/crashes (not just assertion failures), they need `@unittest.skip` instead:
```python
@unittest.skip("TODO: RUSTPYTHON; panics with 'index out of bounds'")
def test_crashes(self):
...
```
- auto-mark cannot detect panics automatically - check the test output for crash messages
6. **Handle class-specific failures**
- If a test fails only in the C implementation (TestCFoo) but passes in the Python implementation (TestPyFoo), or vice versa, move the marker to the specific subclass:
```python
# Base class - no marker here
class TestFoo:
def test_something(self):
...
class TestPyFoo(TestFoo, PyTest): pass
class TestCFoo(TestFoo, CTest):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_something(self):
return super().test_something()
```
7. **Commit the test fixes**
- Run: `git add -u && git commit -m "Mark failing tests"`
- This creates a separate commit for the test markers added in steps 2-6
## Example Usage
```
# Using module names (recommended)
/upgrade-pylib inspect
/upgrade-pylib json
/upgrade-pylib asyncio
# Using library paths (alternative)
/upgrade-pylib cpython/Lib/inspect.py
/upgrade-pylib cpython/Lib/json/
```
## Example: Restoring RUSTPYTHON changes
When git diff shows removed RUSTPYTHON-specific code like:
```diff
-# XXX RUSTPYTHON: we don't import _json as fresh since...
-cjson = import_helper.import_fresh_module('json') #, fresh=['_json'])
+cjson = import_helper.import_fresh_module('json', fresh=['_json'])
```
You should restore the RustPython version:
```python
# XXX RUSTPYTHON: we don't import _json as fresh since...
cjson = import_helper.import_fresh_module('json') #, fresh=['_json'])
```
## Notes
- The cpython/ directory should contain the CPython source that we're syncing from
- `scripts/update_lib` package handles patching and auto-marking:
- `quick` - Combined patch + auto-mark (recommended)
- `migrate` - Only migrate (patch), no test running
- `auto-mark` - Only run tests and mark failures
- `copy-lib` - Copy library files (not tests)
- The patching:
- Transfers `@unittest.expectedFailure` and `@unittest.skip` decorators with `TODO: RUSTPYTHON` markers
- Adds `import unittest # XXX: RUSTPYTHON` if needed for the decorators
- **Limitation**: If a class was restructured (e.g., method overrides removed), update_lib will warn and skip those markers
- The smart auto-mark:
- Marks NEW test failures automatically (tests that didn't exist before)
- Does NOT mark regressions (existing tests that now fail) - these are warnings
- Removes `@unittest.expectedFailure` from tests that now pass
- The script does NOT preserve all RustPython-specific changes - you must review `git diff` and restore them
- Common RustPython markers to look for:
- `# XXX: RUSTPYTHON` or `# XXX RUSTPYTHON` - Inline comments for code modifications
- `# TODO: RUSTPYTHON` - Test skip/failure markers
- Any code with `RUSTPYTHON` in comments that was removed in the diff
- **Important**: Not all changes in the git diff need to be restored. Only restore changes that have explicit `RUSTPYTHON` comments. Other changes are upstream CPython updates.
+7
View File
@@ -4,6 +4,7 @@ argdefs
argtypes
asdl
asname
ASNATIVEBYTES
atopen
atext
attro
@@ -139,6 +140,8 @@ metavars
miscompiles
mult
multibytecodec
mystricmp
mystrnicmp
nameobj
nameop
nargsf
@@ -169,6 +172,7 @@ nvars
opname
opnames
orelse
osmodule
outparam
outparm
paramfunc
@@ -189,11 +193,14 @@ pycore
pyinner
pydecimal
pyerrors
pyframe
Pyfunc
pylifecycle
pymain
pymem
pyrepl
pystate
pystrcmp
PYTHONTRACEMALLOC
PYTHONUTF8
pythonw
+1
View File
@@ -50,6 +50,7 @@ modpow
msvc
muldiv
nanos
noalias
nonoverlapping
objclass
peekable
+1
View File
@@ -27,6 +27,7 @@ pystr
pystruct
pystructseq
pytype
qsbr
rustix
struc
zelf
+10
View File
@@ -59,6 +59,7 @@
"alnum",
"csock",
"coro",
"Crnl",
"dedentations",
"dedents",
"deduped",
@@ -66,8 +67,13 @@
"deoptimize",
"emscripten",
"excs",
"fdigits",
"flufl",
"fnfe",
"fsdefault",
"ifexp",
"implicits",
"inity",
"interps",
"jitted",
"jitting",
@@ -76,8 +82,12 @@
"mcache",
"oparg",
"opargs",
"pointee",
"pyc",
"qmark",
"reborrow",
"reborrows",
"reparenting",
"reraises",
"reraising",
"significand",
+10 -9
View File
@@ -1,10 +1,13 @@
Lib/** linguist-vendored
Cargo.lock linguist-generated
*.snap linguist-generated -merge
vm/src/stdlib/ast/gen.rs linguist-generated -merge
Lib/*.py text working-tree-encoding=UTF-8 eol=LF
**/*.rs text working-tree-encoding=UTF-8 eol=LF
crates/rustpython_doc_db/src/*.inc.rs linguist-generated=true
crates/vm/src/stdlib/ast/gen.rs linguist-generated -merge
crates/doc/src/data.inc.rs generated
crates/compiler-core/src/bytecode/opcode_metadata.rs generated
.github/workflows/*.lock.yml linguist-generated=true merge=ours
# Binary data types
*.aif binary
@@ -45,10 +48,11 @@ Lib/venv/scripts/posix/* text eol=lf
# Language aware diff headers
# https://tekin.co.uk/2020/10/better-git-diff-output-for-ruby-python-elixir-and-more
# https://gist.github.com/tekin/12500956bd56784728e490d8cef9cb81
*.css diff=css
*.html diff=html
*.py diff=python
*.md diff=markdown
*.css diff=css
*.html diff=html
*.py diff=python
*.md diff=markdown
*.inc.rs diff=rust
# Generated files
# https://github.com/github/linguist/blob/master/docs/overrides.md
@@ -66,6 +70,3 @@ 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
.github/workflows/*.lock.yml linguist-generated=true merge=ours
+40 -7
View File
@@ -29,6 +29,10 @@ inputs:
description: Install gcc-aarch64-linux-gnu (gcc-aarch64-linux-gnu)
required: false
default: "false"
gcc-mingw-w64-x86-64:
description: Install gcc-mingw-w64-x86-64 (gcc-mingw-w64-x86-64)
required: false
default: "false"
clang:
description: Install clang (clang)
required: false
@@ -39,11 +43,40 @@ runs:
- name: Install Linux dependencies
shell: bash
if: ${{ runner.os == 'Linux' }}
run: >
sudo apt-get update
env:
GCC_MULTILIB: ${{ inputs.gcc-multilib }}
MUSL_TOOLS: ${{ inputs.musl-tools }}
CLANG: ${{ inputs.clang }}
GCC_AARCH64_LINUX_GNU: ${{ inputs.gcc-aarch64-linux-gnu }}
GCC_MINGW_W64_X86_64: ${{ inputs.gcc-mingw-w64-x86-64 }}
run: |
if ! sudo apt-get update; then
echo "::warning::apt-get update failed; disabling nonessential Microsoft apt sources and retrying"
for source in /etc/apt/sources.list.d/*microsoft* /etc/apt/sources.list.d/*azure-cli*; do
if [ -e "$source" ]; then
sudo mv "$source" "$source.disabled"
fi
done
sudo apt-get update
fi
sudo apt-get install --no-install-recommends
${{ fromJSON(inputs.gcc-multilib) && 'gcc-multilib' || '' }}
${{ fromJSON(inputs.musl-tools) && 'musl-tools' || '' }}
${{ fromJSON(inputs.clang) && 'clang' || '' }}
${{ fromJSON(inputs.gcc-aarch64-linux-gnu) && 'gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross' || '' }}
packages=()
if [[ "$GCC_MULTILIB" == "true" ]]; then
packages+=(gcc-multilib)
fi
if [[ "$MUSL_TOOLS" == "true" ]]; then
packages+=(musl-tools)
fi
if [[ "$CLANG" == "true" ]]; then
packages+=(clang)
fi
if [[ "$GCC_AARCH64_LINUX_GNU" == "true" ]]; then
packages+=(gcc-aarch64-linux-gnu linux-libc-dev-arm64-cross libc6-dev-arm64-cross)
fi
if [[ "$GCC_MINGW_W64_X86_64" == "true" ]]; then
packages+=(gcc-mingw-w64-x86-64)
fi
if ((${#packages[@]})); then
sudo apt-get install --no-install-recommends "${packages[@]}"
fi
+2
View File
@@ -24,6 +24,7 @@ updates:
- "sha1"
- "sha2"
- "sha3"
- "shake"
- "blake2"
- "hmac"
- "pbkdf2"
@@ -88,6 +89,7 @@ updates:
- "getrandom"
- "mt19937"
- "rand*"
- "chacha20"
rayon:
patterns:
- "rayon*"
+67 -51
View File
@@ -33,8 +33,8 @@ env:
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
FORCE_COLOR: 1
jobs:
determine_changes:
@@ -44,7 +44,7 @@ jobs:
# 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
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
@@ -88,14 +88,14 @@ jobs:
os: [macos-latest, ubuntu-latest, windows-2025]
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
@@ -198,13 +198,17 @@ jobs:
target: aarch64-unknown-linux-gnu
dependencies:
gcc-aarch64-linux-gnu: true
- os: ubuntu-latest
target: x86_64-pc-windows-gnu
dependencies:
gcc-mingw-w64-x86-64: true
- os: macos-latest
target: aarch64-apple-ios
- os: macos-latest
target: x86_64-apple-darwin
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -216,9 +220,10 @@ jobs:
gcc-multilib: ${{ matrix.dependencies.gcc-multilib || false }}
musl-tools: ${{ matrix.dependencies.musl-tools || false }}
gcc-aarch64-linux-gnu: ${{ matrix.dependencies.gcc-aarch64-linux-gnu || false }}
gcc-mingw-w64-x86-64: ${{ matrix.dependencies.gcc-mingw-w64-x86-64 || false }}
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
@@ -295,6 +300,8 @@ jobs:
- os: macos-latest
extra_test_args:
- '-u all'
- '--timeout 600'
- '--dont-add-python-opts'
env_polluting_tests:
- test_set
skips: []
@@ -302,30 +309,31 @@ jobs:
- os: ubuntu-latest
extra_test_args:
- '-u all'
- '--timeout 600'
- '--dont-add-python-opts'
env_polluting_tests:
- test_set
skips: []
timeout: 60
- os: windows-2025
extra_test_args: [] # TODO: Enable '-u all'
extra_test_args:
- '-u all'
- '--timeout 600'
- '--dont-add-python-opts'
env_polluting_tests:
- test_set
skips:
- test_rlcompleter
- test_pathlib # panic by surrogate chars
- test_posixpath # OSError: (22, 'The filename, directory name, or volume label syntax is incorrect. (os error 123)')
- test_venv # couple of failing tests
skips: []
timeout: 50
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
@@ -340,13 +348,14 @@ jobs:
# Windows runners randomly crashes, https://github.com/actions/cache/issues/1754
continue-on-error: true
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install macOS dependencies
uses: ./.github/actions/install-macos-deps
with:
openssl: true
# Keep features in sync with update-caches.yml CARGO_ARGS.
- name: build rustpython
run: cargo build --release --verbose --features=threading,jit ${{ env.CARGO_ARGS }}
@@ -363,7 +372,7 @@ jobs:
- name: Run CPython tests
run: |
target/release/rustpython -m test -j ${{ steps.cores.outputs.cores }} ${{ join(matrix.extra_test_args, ' ') }} --slowest --fail-env-changed --timeout 600 -v -x ${{ env.FLAKY_MP_TESTS }} ${{ join(matrix.skips, ' ') }}
target/release/rustpython -u -m test --slow-ci -j ${{ steps.cores.outputs.cores }} ${{ join(matrix.extra_test_args, ' ') }} -x ${{ env.FLAKY_MP_TESTS }} ${{ join(matrix.skips, ' ') }}
timeout-minutes: ${{ matrix.timeout }}
env:
RUSTPYTHON_SKIP_ENV_POLLUTERS: true
@@ -374,7 +383,7 @@ jobs:
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 }}
target/release/rustpython -u -m test --slow-ci -j 1 ${{ join(matrix.extra_test_args, ' ') }} ${{ env.FLAKY_MP_TESTS }}
status=$?
set -e
@@ -399,7 +408,7 @@ jobs:
for thing in "${target_array[@]}"; do
for i in $(seq 1 10); do
set +e
target/release/rustpython -m test -j 1 --slowest --fail-env-changed --timeout 600 -v "${thing}"
target/release/rustpython -u -m test --slow-ci -u all -j 1 --timeout 600 --dont-add-python-opts "${thing}"
exit_code=$?
set -e
if [ "${exit_code}" -eq 3 ]; then
@@ -434,6 +443,17 @@ jobs:
target/release/rustpython -m ensurepip
target/release/rustpython -c "import pip"
- if: runner.os == 'Windows'
name: Check pip HTTPS with the Windows trust store
run: >-
target/release/rustpython -m pip download
--disable-pip-version-check
--no-cache-dir
--no-deps
--only-binary=:all:
--dest "$env:RUNNER_TEMP\rustpython-pip-smoke"
six
- if: runner.os != 'Windows'
name: Check if pip inside venv is functional
run: |
@@ -462,7 +482,7 @@ jobs:
- ubuntu-latest
- windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -471,7 +491,7 @@ jobs:
components: clippy
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
@@ -500,13 +520,13 @@ jobs:
needs.determine_changes.outputs.rust_code == 'true' ||
github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: cargo-bins/cargo-binstall@aaa84a43aec4955a42c5ffc65d258961e39f276e # v1.19.1
- uses: cargo-bins/cargo-binstall@ead08b90bd7b2e6d81963fb9cf0b7239f66d5db4 # v1.21.0
- name: cargo shear
run: |
@@ -523,11 +543,11 @@ jobs:
pull-requests: write
security-events: write # for zizmor
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- uses: dtolnay/rust-toolchain@stable
with:
@@ -537,23 +557,17 @@ jobs:
uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1.72.0
- name: zizmor
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
- name: restore prek cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
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
id: prek
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
uses: j178/prek-action@e98a699c41eb69ab013a45817a0406469a748f8d # v2.0.5
with:
cache: false
show-verbose-logs: false
@@ -569,7 +583,7 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Clone CPython
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: python/cpython
path: cpython
@@ -583,7 +597,7 @@ jobs:
- 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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/prek
@@ -594,7 +608,7 @@ jobs:
- name: reviewdog
if: ${{ !cancelled() }}
uses: reviewdog/action-suggester@aa38384ceb608d00f84b4690cacc83a5aba307ff # v1.24.0
uses: reviewdog/action-suggester@2558ba17e65a9039e73764a73009fc05fef28a46 # v1.24.3
with:
level: warning
fail_level: error
@@ -607,7 +621,7 @@ jobs:
env:
NIGHTLY_CHANNEL: nightly
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -617,7 +631,7 @@ jobs:
components: miri
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
@@ -642,7 +656,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -651,7 +665,7 @@ jobs:
components: clippy
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
@@ -676,12 +690,12 @@ jobs:
mkdir geckodriver
tar -xzf geckodriver-v0.36.0-linux64.tar.gz -C geckodriver
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- run: python -m pip install -r requirements.txt
working-directory: ./wasm/tests
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
package-manager-cache: false
@@ -691,7 +705,7 @@ jobs:
run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT"
- name: Restore npm cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
# don't restore on main or release
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/release'
with:
@@ -746,7 +760,7 @@ jobs:
- 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
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: node-${{ runner.os }}-wasm-demo-${{ hashFiles('wasm/demo/package-lock.json') }}
@@ -757,7 +771,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -766,7 +780,7 @@ jobs:
target: wasm32-wasip1
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
@@ -790,11 +804,13 @@ 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 --profile wasm-release --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib,stdio,importlib,host_env --verbose
- name: run snippets
run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py"
run: |
wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py"
wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_time.py"
- name: run cpython unittest
run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/release/rustpython.wasm -- "$(pwd)/Lib/test/test_int.py"
run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/wasm-release/rustpython.wasm -- "$(pwd)/Lib/test/test_int.py"
cargo_doc:
needs:
@@ -809,14 +825,14 @@ jobs:
name: cargo doc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Restore cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
+17 -16
View File
@@ -15,7 +15,6 @@ on:
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
jobs:
# codecov collects code coverage data from the rust tests, python snippets and python test suite.
@@ -25,18 +24,20 @@ jobs:
runs-on: ubuntu-latest
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
env:
INSTA_WORKSPACE_ROOT: ${{ github.workspace }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@b550161ef8a7bc4f2a671c0b03a18ac9ccedea1e # v2.79.1
- uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0
with:
tool: cargo-llvm-cov
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- run: sudo apt-get update && sudo apt-get -y install lcov
@@ -56,7 +57,7 @@ jobs:
- name: Upload to Codecov
if: ${{ github.event_name != 'pull_request' }}
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: ./codecov.lcov
@@ -66,7 +67,7 @@ jobs:
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true
@@ -84,7 +85,6 @@ jobs:
if: ${{ github.event_name != 'pull_request' }}
env:
SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }}
GITHUB_ACTOR: ${{ github.actor }}
run: |
echo "$SSHKEY" >~/github_key
chmod 600 ~/github_key
@@ -94,7 +94,7 @@ jobs:
cd website
cp ../extra_tests/cpython_tests_results.json ./_data/regrtests_results.json
git add ./_data/regrtests_results.json
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update regression test results" --author="$GITHUB_ACTOR"; then
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update regression test results"; then
git push
fi
@@ -104,13 +104,13 @@ jobs:
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: build rustpython
run: cargo build --release --verbose
@@ -126,7 +126,6 @@ jobs:
if: ${{ github.event_name != 'pull_request' }}
env:
SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }}
GITHUB_ACTOR: ${{ github.actor }}
run: |
echo "$SSHKEY" >~/github_key
chmod 600 ~/github_key
@@ -157,7 +156,7 @@ jobs:
}
EOF
git add -A
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update what is left results" --author="$GITHUB_ACTOR"; then
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update what is left results"; then
git push
fi
@@ -167,13 +166,13 @@ jobs:
# Disable this scheduled job when running on a fork.
if: ${{ github.repository == 'RustPython/RustPython' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- run: cargo install cargo-criterion
@@ -203,6 +202,8 @@ jobs:
if: ${{ github.event_name != 'pull_request' }}
env:
SSHKEY: ${{ secrets.ACTIONS_TESTS_DATA_DEPLOY_KEY }}
COMMIT_SHA: ${{ github.sha }}
REF_NAME: ${{ github.ref_name }}
run: |
echo "$SSHKEY" >~/github_key
chmod 600 ~/github_key
@@ -214,8 +215,8 @@ jobs:
cp -r ../target/criterion ./assets/criterion
printf '{\n "generated_at": "%s",\n "rustpython_commit": "%s",\n "rustpython_ref": "%s"\n}\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
"${{ github.sha }}" \
"${{ github.ref_name }}" > ./_data/criterion-metadata.json
"$COMMIT_SHA" \
"$REF_NAME" > ./_data/criterion-metadata.json
git add ./assets/criterion ./_data/criterion-metadata.json
if git -c user.name="Github Actions" -c user.email="actions@github.com" commit -m "Update benchmark results"; then
git push
+18 -9
View File
@@ -6,6 +6,8 @@ on:
paths:
- "Lib/**"
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
@@ -18,7 +20,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout base branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Use base branch for scripts (security: don't run PR code with elevated permissions)
ref: ${{ github.event.pull_request.base.ref }}
@@ -26,13 +28,17 @@ jobs:
persist-credentials: false
- name: Fetch PR head
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
git fetch origin ${{ github.event.pull_request.head.sha }}
git fetch origin "$PR_HEAD_SHA"
- name: Checkout PR Lib files
env:
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Checkout only Lib/ directory from PR head for accurate comparison
git checkout ${{ github.event.pull_request.head.sha }} -- Lib/
git checkout "$PR_HEAD_SHA" -- Lib/
- name: Get target CPython version
id: cpython-version
@@ -41,7 +47,7 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Checkout CPython
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: python/cpython
path: cpython
@@ -51,14 +57,17 @@ jobs:
- name: Get changed Lib files
id: all-changed-files
env:
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
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'
git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" -- 'Lib/*.py' 'Lib/**/*.py'
echo 'EOF'
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Parse changed files
@@ -98,7 +107,7 @@ jobs:
- name: Setup Python
if: steps.changed-files.outputs.modules != ''
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Run deps check
if: steps.changed-files.outputs.modules != ''
@@ -114,7 +123,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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: lib-deps-check
number: ${{ github.event.pull_request.number }}
@@ -131,7 +140,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@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: lib-deps-check
number: ${{ github.event.pull_request.number }}
+4 -4
View File
@@ -52,7 +52,7 @@ jobs:
# target: aarch64-pc-windows-msvc
fail-fast: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -91,7 +91,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
@@ -114,7 +114,7 @@ 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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
package-manager-cache: false
@@ -156,7 +156,7 @@ jobs:
permissions:
contents: write # for creating a release
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
+4 -3
View File
@@ -19,6 +19,7 @@ env:
CARGO_PROFILE_TEST_DEBUG: 0
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_RELEASE_DEBUG: 0
# Keep feature list in sync with CI's release build in .github/workflows/ci.yaml.
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:
@@ -39,14 +40,14 @@ jobs:
target: ""
steps:
- name: Checkout RustPython main branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: RustPython/RustPython
ref: main
persist-credentials: false
- name: Setup Rust
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.toolchain }}
target: ${{ matrix.target }}
@@ -66,7 +67,7 @@ jobs:
run: cargo build --profile release ${{ env.CARGO_ARGS }}
- name: Save cache
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/bin/
+3 -3
View File
@@ -30,13 +30,13 @@ jobs:
- windows-latest
- macos-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
sparse-checkout: |
crates/doc
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ inputs.python-version }}
@@ -58,7 +58,7 @@ jobs:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true
ref: ${{ inputs.base-ref }}
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
if: ${{ github.repository == 'RustPython/RustPython' }}
steps:
- name: Clone RustPython
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: rustpython
persist-credentials: false
@@ -37,7 +37,7 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Clone CPython
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: python/cpython
path: cpython
+9 -9
View File
@@ -58,7 +58,7 @@ jobs:
comment_repo: ""
steps:
- name: Setup Scripts
uses: github/gh-aw/actions/setup@2c1a237d2048b0e2412e7d7528892ea1257840e2 # v0.74.4
uses: github/gh-aw/actions/setup@8b820ae1073f301991aaf2f307f7e271f618bb9f # v0.82.14
with:
destination: /opt/gh-aw/actions
- name: Check workflow file timestamps
@@ -99,22 +99,22 @@ 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@8b820ae1073f301991aaf2f307f7e271f618bb9f # v0.82.14
with:
destination: /opt/gh-aw/actions
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.14'
- name: Create gh-aw temp directory
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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
key: cpython-lib-${{ env.PYTHON_VERSION }}
path: cpython
@@ -806,7 +806,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@8b820ae1073f301991aaf2f307f7e271f618bb9f # v0.82.14
with:
destination: /opt/gh-aw/actions
- name: Download agent output artifact
@@ -927,7 +927,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@8b820ae1073f301991aaf2f307f7e271f618bb9f # v0.82.14
with:
destination: /opt/gh-aw/actions
- name: Download agent artifacts
@@ -1039,7 +1039,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@8b820ae1073f301991aaf2f307f7e271f618bb9f # v0.82.14
with:
destination: /opt/gh-aw/actions
- name: Download agent output artifact
@@ -1061,7 +1061,7 @@ jobs:
path: /tmp/gh-aw/
- name: Checkout repository
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request'))
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ github.token }}
persist-credentials: false
+1 -1
View File
@@ -52,7 +52,7 @@ cache:
- cpython-lib-
env:
PYTHON_VERSION: "v3.14.4"
PYTHON_VERSION: "v3.14.6"
ISSUE_ID: "6839"
---
+5
View File
@@ -1,4 +1,9 @@
rules:
excessive-permissions:
ignore:
# pull_request_target is needed to post PR comments with pull-requests: write.
# Workflow-level permissions: {} restricts defaults; only the job has write access.
- lib-deps-check.yaml:3
unpinned-uses:
config:
policies:
+5 -2
View File
@@ -8,7 +8,9 @@ __pycache__/
.repl_history.txt
.vscode/
wasm-pack.log
.idea/
.idea/*
!.idea/icon.svg
!.idea/vcs.xml
.envrc
flame-graph.html
@@ -27,4 +29,5 @@ Lib/site-packages/*
Lib/test/data/*
!Lib/test/data/README
cpython/
.claude/scheduled_tasks.lock
.claude/
docs/superpowers/
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAJ5lWElmTU0AKgAAAAgABAEaAAUAAAABAAAAPgEbAAUAAAABAAAARgEoAAMAAAABAAIAAIdpAAQAAAABAAAATgAAAAAAAAEsAAAAAQAAASwAAAABAASShgAHAAAAGQAAAISgAQADAAAAAQABAACgAgAEAAAAAQAAAICgAwAEAAAAAQAAAIAAAAAAQVNDSUkAAABDcmVhdGVkIHdpdGggR0lNUADcIp6GAAAACXBIWXMAAC4jAAAuIwF4pT92AAABaWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDxleGlmOlVzZXJDb21tZW50PkNyZWF0ZWQgd2l0aCBHSU1QPC9leGlmOlVzZXJDb21tZW50PgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KLa3NoAAAHSZJREFUeAHtXQtgVMXVPnPv3VcSQhDkJaAIiDZAAmilgBosAiLa2haK9rf8WgsERdu/Pn7tX01/sb9FW62Wd/vbap9Sq/IIlFZFAa0PIIBR8VF5+EAeEkIem929d/qdu9llEzZmc+/d7G6yA5O9j5m5M+ecOXPmzJkzRNmQhUAWAlkIZCHQSSEgOmm7U9rso1+lggJJ/WuDdCivnA6ksjJKKj/e2b4tiYT/MprfRaetAYMqNIW2+y+nFbVXUN9UwSLLAdoJ8nI6nRJsoEUuopk6vhkyiFRAX8MDf4Ce9dbQFLGRQu1UnehntOhV9sI2BD4oIW+ffDpdSBomJZ2JAs+UgnwkSfP7aahXoXMbgH0X+K5HBeKZEoL0Fuhgm+2PWywgywEsAo6zoVergQYqFAZdYAi6CIgejscD3ArliGaQNcD/g+j1eF6Py3/i9QbdoBd9ObRdrKR6G9WwlTXLAdoIvs8mUtcuHjoPCJ/kr6cSdOYRLpU8XAwQStypG/AbG5gWmCAURLyqBetfj/F/uVhNVbHpUnHNdcuGBCBQN5XO11T6Nlj7FCDzTGbjkV6NHm0GHtM5MqI58Hs9HDlJHWKuhnccQSSHcb9DVWgd0q11r6bdeBwpCq/aJzRWtX0+lolfOT6Jerq8tECRNAtId7PwxkhlTDHw0JNNpHPvD0o6gkfv48UuyAEV+N2DLu8Hgo8j2VFS6Tq3oNs5LwiJgHwUAFmggTZCCLwkFUJglgCAg5ZCzVQqdqv0OBA/LADezpyde7cLkVk6BLoQft4GLp/Fqw26QhU5q+gTPGMcxw310+hacInvgyjOQFn8b2dQp5ty19LWuBmS/BB1zYZ4EKi+jMb6FFqJHt6XpXUGFEvuIIQ6IP+1kKRyvPu7qxpS/EbyxyujpWcfYLbQy0c9fD6i9+rp0JB11NBS2mQ/zxJAHAjXAfmQ5J8Gok9lgY57PLq0BNt+HGzgAU857YqTLSMfZQmgGdqglRvphlAG9tyHke8JC3tHoaG50bea/tAsecbfZgkgBoVyIg3QffQcxuhBPOazpI+uv08EaIbYQK/EJO0wl51GDzB4/sOerg0er6KFfEFD8SqKSzN0luHCgSfylYcfLuslqgfVobsrGO+hxat9yXXmnXNPv/1A8cD9Qzil1BSMDkZ3SPk9pCHdoBMzoCCWEY/hd3fF0tI94afp/zcKgPSvauI1LJy3KE9TPUMUPTSKDFEsFTkUPbkPSsgHCiF6CTeugWJTtotI7KJA6rmRG/5akJRgtaLValKPdBSGF197BFMITwVigzRYUKjCn81CqCt1I7R+59LSg7FJ0u26WQvSrXqJ12fY9Q/1Ut3eEiHFNCDmi0DCQEXjpRZhSm8E5DA/D/+PRfOJb7DipnmIKHWaPDfTxUnMiUAUiqLxZ8AT9D0GGf+9Y/HcP/NtOoaMJgBm610M93iSytWS5GShaKcJIEAaOpAOhLO2pd0DCAB0J3WMI1wXKRtIhq6rWDIvLQXIjCSA0bOXDQBTngncXoVYrKhhgJtIb3eEx3wQCDeCATq063nqNng0efJPDQ8TUj+iG7IEw8EbManT4jIytqVFZVqrxIj5K/opujFfJzFLUbVejHDuaUYo0FrWdnnPvUkaIao98D516X8OBIUwN1I0d3dFBm7F61ntUpE2fCQjOAALdS6plQpFfA9svi8jPeW9vUUgQ7vLiDeFjcYhSCggDONTkMfoHUtLP2oxa/wXp+PxeYjPIh6Nn8T6U57ppnUYNW/p19zCtRnj6kJUtC/39vRFPoMSqG8uf+AexNtLy8k/KwbY5mwC9zwDjY0QXKOBucYOxJWIY6JPHbxI2yFg1LxFgzFb+wnAOR3QSxs2bxX2PDP4dPv6W5D/q4hsAzgU0YsYK6kyRw4ibkbMQ7wacSPizxCZAzge0pIARs5dMtMQ6v2KovaT5vjOU7gMDxgWGqoOTW1sBVsA7UVkG4HYYZiJgYniuzHP2WqYl5mTEmI/npQPtKXQ0bMf6KGrXe4VimICgKdzmRhQf57+oW/HdG5wsZr9b13/3uoHt6BNxxFZQRSPsllBdQbiWMRZiCWIHO5CvMe8cvBP2hDAiNnLzlc1sVSoWrERYi4YAzwHG9weRfmrDpLmzSXNBy7ORACCIMP4JCe3Z/FLD3ytrZrBYtSZOcdziP90uv5MbSkPI+cs/YZwq38SQhkodUZ+JgdJe/7xGzB2hfJ6n8nSP9YVXMB/aO3rD3/7MQst4yFgE+KHFvK2miXlMkDx3CXfEqr6K3R4L8+hMz8IGjDhGoJaGoLrifYohngyHduWUgIoLl02HdOjFZD0gfx4w2E6gqz1OnnyuvFk0GT/UALxDGZDUNXXt56z/VOkTAYowvxeESpYosztSMg3UQj2r6gq2D8Tgb460KD+Z+Wvr/+s/dHb+hdTwgFGli6+AmPkb1G9dkI+tHO8rCdUU0sXDyxs/cPWvrGBrYHYzPtzA4S8qGKKBb6w+veoocsXsDK5XNHpH0B+2go2rTXvc9tu5eXI0uUXEeztAKmCZE/zoDYG4lmJFIRth2Qh6l189yNw52NSyKghJ/BM02u3zehOdQPY7Bs4hDZG+Nd4C3+/z93zoNcInNDOSaEiL1uD1xlSBpB0P6jkoNSlrphlai4p9bcyxSikXQlgVOniEYbQyoGU08zlUisU1FoeYM+UusNTye1o4NNSKP9wi8DuVxfd2KJCRU6i66CQ/XV0Zs6QkdDIVdMNYhPtbO2zmfq+3QigcPYvBrg133pg55ykTPUiiNeDVdCtPQNu/NtQz25bKstmJLRUyMwbNvv3+VS6jTdw8miAzZy82aMKQ/lPPAFaDLvA2kxFdEv1bhcCGHHLY7lqbd0qobkvdn7pNmqAcQzat0dVnZZtXT7n7ZYa3NpzEME9MPr7n8YtXaYMwMahIIQK7A76JRQnz2BPH2/r6hChXQiguHTJYkXzlDqNfNMuj+dbUv7ZUGjBjkWzK53ACpw4zMa2rfsx0OebW7hRKIjC3BUEYXE/bjfAKHQ9Nohu9X5MH4qt5gJOq5+uv5wGQNXRHXsDQ9Ua7Tl1lakSbjVfMhMknQCK5yyah56/KDzVayZm22iZOb/Wg/8Shrht+9LZjitZApfTGIwqCzFNuoBXJHhrNwdzcyeIgWd4eFaN4WYvSPAVpF2PrWGv5jxjEkg4cePf6ql0lkeje8FBvowdRV2QVoKTvA+ucmfOGnqqSeJ2vkkqAYy8YemXpFQ24CN50amSzQZCXYzRmhdbjD8aqrht5yPfZek+KeHdS8nT30WzgLjvA/FnM7ACQHzEeJRnljxN5MjPgNwj+NmB2xdgAvASCOIz1aD/AMKvwh7D3nALYy4NcGWZoyCPH0QwzbcmOUu9iQAFdU1OKPzOr05xu/XnSVVHOCXxmyyfZDUULLdWLJmzPDk1P7nUI5dSfp5G0/DmGsTxQB6v1UeQHiUIJgTmEEC4yTEgR+helVTmHixT8LuIlTEuSWByCQcTP/KsoQVcXioCOFxyguYOlYH1j3Bq3GfkQ8g7igF/5o4lczYkp9bxS+2+DqyezG1hf/CDnUOrMwGzjLFQLgzH8yFAskkQrEMwezkemggGc4jIEHwPDvEOhgvmEm4QSbUrRHtRzna8SlngejkeoOmbSIqrHGwa5rpors0QFvZktdCD39y27Ia00amzi5iGWhoE2pyAVjJRfBFNPcMDhSM3mZsOpJu9n7kDBMeXsBX8BznrnF/WtQpixwmg+HuPFlCDf5NQXcOcYP2syUOoEdK4etviuautNrQ98kkMFfUqFYLNjwHyR+Kbg0AKAyHw9eEhgFXLIIgaAP2X9XX0QP6zybP0SbS9zhPA3CX3Ki7PnY6wfgh7sLBt0EP6tTuXzf1joo1Kp3SY+l2E3v934N7FsgDLACwAQvh7B15F5qVSAGQ4md3LKYDBsOML6LE3Gk4YdWCQBPIxihg/yFTkM1yxpfwF9PpZ4AiHvJC4GPk8NLg0OgvywPlOwd5qOU4KgQLKmLtU1ZUfNuS0WqVwPraiBRf5PyyqLLJXUupz56ymP/qvoNdg4vhNIH80uEA32LruPJJDD6a6do4RAAS/C2H79vWwPZ+9ZoWVPIEnCnp/ere9ktInt3cVvYfa3Js+NQrXxBkCKCLine truncated
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Generated
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="IssueNavigationConfiguration">
<option name="links">
<list>
<IssueNavigationLink>
<option name="issueRegexp" value="#(\d+)" />
<option name="linkRegexp" value="https://github.com/RustPython/RustPython/pull/$1" />
</IssueNavigationLink>
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+3 -3
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.22
hooks:
- id: ruff-format
priority: 0
@@ -63,7 +63,7 @@ repos:
- manual
- repo: https://github.com/streetsidesoftware/cspell-cli
rev: v10.0.0
rev: v10.0.1
hooks:
- id: cspell
types: [rust]
@@ -77,7 +77,7 @@ repos:
priority: 0
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.8.3
rev: v3.9.5
hooks:
- id: prettier
files: '^wasm/.*$'
+1 -1
View File
@@ -1 +1 @@
3.14.5
3.14.6
+5 -22
View File
@@ -1,6 +1,6 @@
# GitHub Copilot Instructions for RustPython
# AI Agent Instructions for RustPython
This document provides guidelines for working with GitHub Copilot when contributing to the RustPython project.
This document provides guidelines for AI coding agents (GitHub Copilot, Claude Code, Gemini, etc.) contributing to the RustPython project.
## Project Overview
@@ -13,22 +13,7 @@ RustPython is a Python 3 interpreter written in Rust, implementing Python 3.14.0
## Repository Structure
- `src/` - Top-level code for the RustPython binary
- `vm/` - The Python virtual machine implementation
- `builtins/` - Python built-in types and functions
- `stdlib/` - Essential standard library modules implemented in Rust, required to run the Python core
- `compiler/` - Python compiler components
- `parser/` - Parser for converting Python source to AST
- `core/` - Bytecode representation in Rust structures
- `codegen/` - AST to bytecode compiler
- `Lib/` - CPython's standard library in Python (copied from CPython). **IMPORTANT**: Do not edit this directory directly; The only allowed operation is copying files from CPython.
- `derive/` - Rust macros for RustPython
- `common/` - Common utilities
- `extra_tests/` - Integration tests and snippets
- `stdlib/` - Non-essential Python standard library modules implemented in Rust (useful but not required for core functionality)
- `wasm/` - WebAssembly support
- `jit/` - Experimental JIT compiler implementation
- `pylib/` - Python standard library packaging (do not modify this directory directly - its contents are generated automatically)
See the "Code organization" section in [CONTRIBUTING.md](CONTRIBUTING.md#code-organization) for the current directory layout.
## AI Agent Rules
@@ -265,12 +250,10 @@ cargo run --features jit
### Linux Build and Debug on macOS
See the "Testing on Linux from macOS" section in [DEVELOPMENT.md](DEVELOPMENT.md#testing-on-linux-from-macos).
See the "Testing on Linux from macOS" section in [CONTRIBUTING.md](CONTRIBUTING.md#testing-on-linux-from-macos).
### Building venvlauncher (Windows)
See DEVELOPMENT.md "CPython Version Upgrade Checklist" section.
**IMPORTANT**: All 4 venvlauncher binaries use the same source code. Do NOT add multiple `[[bin]]` entries to Cargo.toml. Build once and copy with different names.
## Test Code Modification Rules
@@ -301,7 +284,7 @@ If you modify any file under `.github/workflows/`, the change must pass a [zizmo
## Documentation
- Check the [architecture document](/architecture/architecture.md) for a high-level overview
- Read the [development guide](/DEVELOPMENT.md) for detailed setup instructions
- Read the [development guide](/CONTRIBUTING.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
+9 -1
View File
@@ -89,7 +89,15 @@ $ pytest -v
Rust unit tests can be run with `cargo`:
```shell
$ cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher
$ cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi
```
`rustpython-capi` needs to be tested from inside its own directory, since it has a
separate `cargo` config that only applies there:
```shell
$ cd crates/capi
$ cargo test
```
Python unit tests can be run by compiling RustPython and running the test module:
Generated
+673 -988
View File
File diff suppressed because it is too large. Load diff
+100 -55
View File
@@ -49,7 +49,6 @@ 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 }
@@ -60,6 +59,7 @@ rustyline = { workspace = true }
[dev-dependencies]
criterion = { workspace = true }
pyo3 = { workspace = true, features = ["auto-initialize"] }
rustls-graviola = { workspace = true }
rustpython-stdlib = { workspace = true }
ruff_python_parser = { workspace = true }
@@ -79,7 +79,6 @@ path = "src/main.rs"
name = "custom_tls_providers"
path = "examples/custom_tls_providers.rs"
required-features = [
"rustls-graviola",
"rustls/ring",
"rustpython-pylib/freeze-stdlib",
"rustpython-stdlib/ssl-rustls",
@@ -117,6 +116,14 @@ opt-level = 3
[profile.release]
lto = "thin"
[profile.wasm-release]
inherits = "release"
opt-level = "s"
lto = true
codegen-units = 1
strip = true
panic = "abort"
[patch.crates-io]
parking_lot_core = { git = "https://github.com/youknowone/parking_lot", branch = "rustpython" }
# REDOX START, Uncomment when you want to compile/check with redoxer
@@ -172,45 +179,38 @@ rustpython-vm = { path = "crates/vm", default-features = false, version = "0.5.0
rustpython-pylib = { path = "crates/pylib", version = "0.5.0" }
rustpython-stdlib = { path = "crates/stdlib", default-features = false, version = "0.5.0" }
rustpython-sre_engine = { path = "crates/sre_engine", version = "0.5.0" }
rustpython-unicode = { path = "crates/unicode", version = "0.5.0" }
rustpython-wtf8 = { path = "crates/wtf8", version = "0.5.0" }
rustpython-doc = { path = "crates/doc", version = "0.5.0" }
# Use RustPython-packaged Ruff crates from the published fork while keeping
# existing crate names in the codebase.
ruff_python_parser = { package = "rustpython-ruff_python_parser", version = "0.15.8" }
ruff_python_ast = { package = "rustpython-ruff_python_ast", version = "0.15.8" }
ruff_text_size = { package = "rustpython-ruff_text_size", version = "0.15.8" }
ruff_source_file = { package = "rustpython-ruff_source_file", version = "0.15.8" }
# To update ruff crates, comment out the above lines and uncomment the following lines to pull directly from the Ruff repository at the specified commit hash.
# Ruff tag 0.15.8 is based on commit c2a8815842f9dc5d24ec19385eae0f1a7188b0d9
# at the time of this capture. We use the commit hash to ensure reproducible builds.
# ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# 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" }
# Use the RustPython Ruff fork for RustPython public `_ast` metadata.
ruff_python_parser = { package = "rustpython-ruff_python_parser", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_python_ast = { package = "rustpython-ruff_python_ast", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_text_size = { package = "rustpython-ruff_text_size", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
ruff_source_file = { package = "rustpython-ruff_source_file", git = "https://github.com/RustPython/ruff.git", tag = "0.15.19-rustpython" }
der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] }
phf = { version = "0.13.1", default-features = false, features = ["macros"]}
phf = { version = "0.14.0", default-features = false, features = ["macros"]}
adler32 = "1.2.0"
approx = "0.5.1"
ascii = "1.1"
ascii = { version = "1.1", default-features = false }
base64 = "0.22"
blake2 = "0.10.4"
blake2 = "0.11.0-rc.6"
bitflags = "2.11.0"
bitflagset = "0.0.3"
bstr = "1"
bstr = { version = "1", default-features = false, features = ["unicode"] }
bzip2 = "0.6"
chrono = { version = "0.4.44", default-features = false, features = ["clock", "std"] }
console_error_panic_hook = "0.1"
constant_time_eq = "0.4"
cranelift = "0.131.2"
cranelift-jit = "0.131.2"
cranelift-module = "0.131.0"
constant_time_eq = "0.5"
cranelift = "0.132.0"
cranelift-jit = "0.132.0"
cranelift-module = "0.132.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"
digest = "0.11"
dns-lookup = "3.0"
dyn-clone = "1.0.10"
exitcode = "1.1.2"
@@ -220,38 +220,39 @@ 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"] }
getrandom = { version = "0.4", features = ["std"] }
glob = "0.3"
half = "2"
hex = "0.4.3"
hexf-parse = "0.2.1"
hmac = "0.12"
hmac = "0.13"
indexmap = { version = "2.14.0", features = ["std"] }
insta = "1.47"
itertools = "0.14.0"
itertools = { version = "0.15.0", default-features = false, features = ["use_alloc"] }
itoa = "1"
is-macro = "0.3.7"
js-sys = "0.3"
junction = "1.4.2"
junction = "2.0.0"
lexical-parse-float = "1.0.6"
libc = "0.2.186"
libffi = "5"
libloading = "0.9"
liblzma = "0.4"
liblzma-sys = "0.4"
libsqlite3-sys = "0.37"
xz = "0.4.6-rc.0"
xz-sys = "0.4.6-rc.0"
libsqlite3-sys = "0.38"
libz-rs-sys = "0.6"
lock_api = "0.4"
log = "0.4.29"
log = "0.4.30"
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"
malachite-bigint = "0.9.1"
malachite-q = "0.9.1"
malachite-base = "0.9.1"
md-5 = "0.10.1"
memchr = "2.8.0"
malachite-bigint = "0.10.0"
malachite-q = "0.10.0"
malachite-base = "0.10.0"
md-5 = "0.11"
memchr = { version = "2.8.1", default-features = false, features = ["alloc"] }
memmap2 = "0.9.10"
mt19937 = "<=3.2" # upgrade it once rand is upgraded
mt19937 = "3.3"
num-complex = "0.4.6"
num-integer = "0.1.46"
num-traits = "0.2"
@@ -264,22 +265,21 @@ openssl-probe = "0.2.1"
optional = "0.5"
parking_lot = "0.12.3"
paste = "1.0.15"
pbkdf2 = "0.12"
pbkdf2 = "0.13"
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"
pyo3 = "0.29"
quote = "1.0.45"
radium = "1.1.1"
rand = "0.9"
rand_core = { version = "0.9", features = ["os_rng"] }
rand = "0.10"
rapidhash = "4.4.1"
result-like = "0.5.0"
rustix = { version = "1.1", features = ["event", "param", "system"] }
rustix = { version = "1.1", features = ["event", "fs", "param", "system"] }
rustls = { version = "0.23.39", default-features = false }
rustls-graviola = "0.3"
rustls-graviola = "0.4"
rustls-native-certs = "0.8"
rustls-pemfile = "2.2"
rustls-platform-verifier = "0.7"
@@ -288,11 +288,12 @@ serde = { package = "serde_core", version = "1.0.225", default-features = false,
schannel = "0.1.29"
scopeguard = "1"
serde-wasm-bindgen = "0.6.5"
sha-1 = "0.10.0"
sha2 = "0.10.2"
sha3 = "0.10.1"
sha1 = "0.11"
sha2 = "0.11"
sha3 = "0.12"
shake = "0.1"
siphasher = "1"
socket2 = "0.6.3"
socket2 = "0.6.4"
static_assertions = "1.1"
strum = "0.28"
strum_macros = "0.28"
@@ -303,16 +304,14 @@ 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"
thin-vec = "0.2.14"
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"
unic-ucd-age = "0.9.0"
unicode_names2 = "2.0.0"
uuid = "1.23.2"
unicode_names2 = { version = "3", default-features = false, features = ["no_std"] }
widestring = "1.2.0"
windows-sys = "0.61.2"
wasm-bindgen = "0.2.106"
@@ -320,9 +319,9 @@ wasm-bindgen-futures = "0.4"
web-sys = "0.3"
webpki-roots = "1.0"
which = "8"
x509-cert = "0.2.5"
x509-cert = "0.3.0"
x509-parser = "0.18"
xml = "1.2"
xml = "1.3"
writeable = "0.6"
# Lints
@@ -351,29 +350,49 @@ similar_names = "allow"
# restriction lints
alloc_instead_of_core = "warn"
cfg_not_test = "warn"
iter_over_hash_type = "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
collection_is_never_read = "warn"
debug_assert_with_mut_call = "warn"
derive_partial_eq_without_eq = "warn"
imprecise_flops = "warn"
iter_on_empty_collections = "warn"
iter_on_single_items = "warn"
literal_string_with_formatting_args = "warn"
needless_collect = "warn"
or_fun_call = "warn"
redundant_clone = "warn"
search_is_some = "warn"
significant_drop_in_scrutinee = "warn"
single_option_map = "warn"
trait_duplication_in_bounds = "warn"
tuple_array_conversions = "warn"
type_repetition_in_bounds = "warn"
unnecessary_struct_initialization = "warn"
unused_peekable = "warn"
unused_rounding = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
while_float = "warn"
# pedantic lints to enforce gradually
assigning_clones = "warn"
bool_to_int_with_if = "warn"
checked_conversions = "warn"
cloned_instead_of_copied = "warn"
collapsible_else_if = "warn"
comparison_chain = "warn"
copy_iterator = "warn"
doc_link_with_quotes = "warn"
duration_suboptimal_units = "warn"
elidable_lifetime_names = "warn"
enum_glob_use = "warn"
explicit_deref_methods = "warn"
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
filter_map_next = "warn"
@@ -381,11 +400,37 @@ flat_map_option = "warn"
format_collect = "warn"
from_iter_instead_of_collect = "warn"
inconsistent_struct_constructor = "warn"
index_refutable_slice = "warn"
inefficient_to_string = "warn"
ip_constant = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
large_futures = "warn"
large_types_passed_by_value = "warn"
manual_assert = "warn"
manual_instant_elapsed = "warn"
manual_is_variant_and = "warn"
map_unwrap_or = "warn"
match_bool = "warn"
mismatching_type_param_order = "warn"
must_use_candidate = "warn"
mut_mut = "warn"
needless_bitwise_bool = "warn"
needless_for_each = "warn"
non_std_lazy_statics = "warn"
option_as_ref_cloned = "warn"
ptr_offset_by_literal = "warn"
range_minus_one = "warn"
range_plus_one = "warn"
redundant_else = "warn"
ref_option = "warn"
return_self_not_must_use = "warn"
same_functions_in_if_condition = "warn"
single_char_pattern = "warn"
trivially_copy_pass_by_ref = "warn"
unchecked_time_subtraction = "warn"
uninlined_format_args = "warn"
unnecessary_box_returns = "warn"
unnecessary_join = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
+17 -12
View File
@@ -461,8 +461,8 @@ class Buffer(metaclass=ABCMeta):
class _CallableGenericAlias(GenericAlias):
""" Represent `Callable[argtypes, resulttype]`.
This sets ``__args__`` to a tuple containing the flattened ``argtypes``
followed by ``resulttype``.
This sets ``__args__`` to a tuple containing the flattened
``argtypes`` followed by ``resulttype``.
Example: ``Callable[[int, str], float]`` sets ``__args__`` to
``(int, str, float)``.
@@ -927,8 +927,9 @@ class MutableMapping(Mapping):
__marker = object()
def pop(self, key, default=__marker):
'''D.pop(k[,d]) -> v, remove specified key and return the corresponding value.
If key is not found, d is returned if given, otherwise KeyError is raised.
'''D.pop(k[,d]) -> v, remove specified key and return the corresponding
value. If key is not found, d is returned if given, otherwise
KeyError is raised.
'''
try:
value = self[key]
@@ -962,9 +963,12 @@ class MutableMapping(Mapping):
def update(self, other=(), /, **kwds):
''' D.update([E, ]**F) -> None. Update D from mapping/iterable E and F.
If E present and has a .keys() method, does: for k in E.keys(): D[k] = E[k]
If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v
In either case, this is followed by: for k, v in F.items(): D[k] = v
If E present and has a .keys() method, does:
for k in E.keys(): D[k] = E[k]
If E present and lacks .keys() method, does:
for (k, v) in E: D[k] = v
In either case, this is followed by:
for k, v in F.items(): D[k] = v
'''
if isinstance(other, Mapping):
for key in other:
@@ -1029,8 +1033,8 @@ class Sequence(Reversible, Collection):
yield self[i]
def index(self, value, start=0, stop=None):
'''S.index(value, [start, [stop]]) -> integer -- return first index of value.
Raises ValueError if the value is not present.
'''S.index(value, [start, [stop]]) -> integer -- return first index of
value. Raises ValueError if the value is not present.
Supporting start and stop arguments is optional, but
recommended.
@@ -1138,15 +1142,16 @@ class MutableSequence(Sequence):
self[i], self[n-i-1] = self[n-i-1], self[i]
def extend(self, values):
'S.extend(iterable) -- extend sequence by appending elements from the iterable'
"""S.extend(iterable) -- extend sequence by appending elements from the
iterable"""
if values is self:
values = list(values)
for v in values:
self.append(v)
def pop(self, index=-1):
'''S.pop([index]) -> item -- remove and return item at index (default last).
Raise IndexError if list is empty or index is out of range.
'''S.pop([index]) -> item -- remove and return item at index (default
last). Raise IndexError if list is empty or index is out of range.
'''
v = self[index]
del self[index]
+56 -51
View File
@@ -104,8 +104,8 @@ class DecimalException(ArithmeticError):
anything, though.
handle -- Called when context._raise_error is called and the
trap_enabler is not set. First argument is self, second is the
context. More arguments can be given, those being after
trap_enabler is not set. First argument is self, second is
the context. More arguments can be given, those being after
the explanation in _raise_error (For example,
context._raise_error(NewError, '(-x)!', self._sign) would
call NewError().handle(context, self._sign).)
@@ -222,11 +222,12 @@ class InvalidContext(InvalidOperation):
"""Invalid context. Unknown rounding, for example.
This occurs and signals invalid-operation if an invalid context was
detected during an operation. This can occur if contexts are not checked
on creation and either the precision exceeds the capability of the
underlying concrete representation or an unknown or unsupported rounding
was specified. These aspects of the context need only be checked when
the values are required to be used. The result is [0,qNaN].
detected during an operation. This can occur if contexts are not
checked on creation and either the precision exceeds the capability of
the underlying concrete representation or an unknown or unsupported
rounding was specified. These aspects of the context need only be
checked when the values are required to be used. The result is
[0,qNaN].
"""
def handle(self, context, *args):
@@ -319,8 +320,9 @@ class FloatOperation(DecimalException, TypeError):
Decimal.from_float() or context.create_decimal_from_float() do not
set the flag.
Otherwise (the signal is trapped), only equality comparisons and explicit
conversions are silent. All other mixed operations raise FloatOperation.
Otherwise (the signal is trapped), only equality comparisons and
explicit conversions are silent. All other mixed operations raise
FloatOperation.
"""
# List of public traps and flags
@@ -2898,8 +2900,8 @@ class Decimal(object):
"""Compares self to other using the abstract representations.
This is not like the standard compare, which use their numerical
value. Note that a total ordering is defined for all possible abstract
representations.
value. Note that a total ordering is defined for all possible
abstract representations.
"""
other = _convert_other(other, raiseit=True)
@@ -2970,7 +2972,8 @@ class Decimal(object):
def compare_total_mag(self, other, context=None):
"""Compares self to other using abstract repr., ignoring sign.
Like compare_total, but with operand's sign ignored and assumed to be 0.
Like compare_total, but with operand's sign ignored and assumed to
be 0.
"""
other = _convert_other(other, raiseit=True)
@@ -4107,9 +4110,9 @@ class Context(object):
def abs(self, a):
"""Returns the absolute value of the operand.
If the operand is negative, the result is the same as using the minus
operation on the operand. Otherwise, the result is the same as using
the plus operation on the operand.
If the operand is negative, the result is the same as using the
minus operation on the operand. Otherwise, the result is the same
as using the plus operation on the operand.
>>> ExtendedContext.abs(Decimal('2.1'))
Decimal('2.1')
@@ -4165,16 +4168,17 @@ class Context(object):
def compare(self, a, b):
"""Compares values numerically.
If the signs of the operands differ, a value representing each operand
('-1' if the operand is less than zero, '0' if the operand is zero or
negative zero, or '1' if the operand is greater than zero) is used in
place of that operand for the comparison instead of the actual
operand.
If the signs of the operands differ, a value representing each
operand ('-1' if the operand is less than zero, '0' if the operand
is zero or negative zero, or '1' if the operand is greater than
zero) is used in place of that operand for the comparison instead of
the actual operand.
The comparison is then effected by subtracting the second operand from
the first and then returning a value according to the result of the
subtraction: '-1' if the result is less than zero, '0' if the result is
zero or negative zero, or '1' if the result is greater than zero.
The comparison is then effected by subtracting the second operand
from the first and then returning a value according to the result of
the subtraction: '-1' if the result is less than zero, '0' if the
result is zero or negative zero, or '1' if the result is greater
than zero.
>>> ExtendedContext.compare(Decimal('2.1'), Decimal('3'))
Decimal('-1')
@@ -4237,8 +4241,8 @@ class Context(object):
"""Compares two operands using their abstract representation.
This is not like the standard compare, which use their numerical
value. Note that a total ordering is defined for all possible abstract
representations.
value. Note that a total ordering is defined for all possible
abstract representations.
>>> ExtendedContext.compare_total(Decimal('12.73'), Decimal('127.9'))
Decimal('-1')
@@ -4265,7 +4269,8 @@ class Context(object):
def compare_total_mag(self, a, b):
"""Compares two operands using their abstract representation ignoring sign.
Like compare_total, but with operand's sign ignored and assumed to be 0.
Like compare_total, but with operand's sign ignored and assumed to
be 0.
"""
a = _convert_other(a, raiseit=True)
return a.compare_total_mag(b)
@@ -4923,8 +4928,8 @@ class Context(object):
If either operand is a special value then the general rules apply.
Otherwise, the operands are multiplied together
('long multiplication'), resulting in a number which may be as long as
the sum of the lengths of the two operands.
('long multiplication'), resulting in a number which may be as long
as the sum of the lengths of the two operands.
>>> ExtendedContext.multiply(Decimal('1.20'), Decimal('3'))
Decimal('3.60')
@@ -5200,19 +5205,19 @@ class Context(object):
"""Returns a value equal to 'a' (rounded), having the exponent of 'b'.
The coefficient of the result is derived from that of the left-hand
operand. It may be rounded using the current rounding setting (if the
exponent is being increased), multiplied by a positive power of ten (if
the exponent is being decreased), or is unchanged (if the exponent is
already equal to that of the right-hand operand).
operand. It may be rounded using the current rounding setting (if
the exponent is being increased), multiplied by a positive power of
ten (if the exponent is being decreased), or is unchanged (if the
exponent is already equal to that of the right-hand operand).
Unlike other operations, if the length of the coefficient after the
quantize operation would be greater than precision then an Invalid
operation condition is raised. This guarantees that, unless there is
an error condition, the exponent of the result of a quantize is always
equal to that of the right-hand operand.
operation condition is raised. This guarantees that, unless there
is an error condition, the exponent of the result of a quantize is
always equal to that of the right-hand operand.
Also unlike other operations, quantize will never raise Underflow, even
if the result is subnormal and inexact.
Also unlike other operations, quantize will never raise Underflow,
even if the result is subnormal and inexact.
>>> ExtendedContext.quantize(Decimal('2.17'), Decimal('0.001'))
Decimal('2.170')
@@ -5266,13 +5271,13 @@ class Context(object):
"""Returns the remainder from integer division.
The result is the residue of the dividend after the operation of
calculating integer division as described for divide-integer, rounded
to precision digits if necessary. The sign of the result, if
non-zero, is the same as that of the original dividend.
calculating integer division as described for divide-integer,
rounded to precision digits if necessary. The sign of the result,
if non-zero, is the same as that of the original dividend.
This operation will fail under the same conditions as integer division
(that is, if integer division on the same two operands would fail, the
remainder cannot be calculated).
This operation will fail under the same conditions as integer
division (that is, if integer division on the same two operands
would fail, the remainder cannot be calculated).
>>> ExtendedContext.remainder(Decimal('2.1'), Decimal('3'))
Decimal('2.1')
@@ -5306,9 +5311,9 @@ class Context(object):
is chosen). If the result is equal to 0 then its sign will be the
sign of a.
This operation will fail under the same conditions as integer division
(that is, if integer division on the same two operands would fail, the
remainder cannot be calculated).
This operation will fail under the same conditions as integer
division (that is, if integer division on the same two operands
would fail, the remainder cannot be calculated).
>>> ExtendedContext.remainder_near(Decimal('2.1'), Decimal('3'))
Decimal('-0.9')
@@ -5366,8 +5371,8 @@ class Context(object):
def same_quantum(self, a, b):
"""Returns True if the two operands have the same exponent.
The result is never affected by either the sign or the coefficient of
either operand.
The result is never affected by either the sign or the coefficient
of either operand.
>>> ExtendedContext.same_quantum(Decimal('2.17'), Decimal('0.001'))
False
@@ -5439,8 +5444,8 @@ class Context(object):
def sqrt(self, a):
"""Square root of a non-negative number to context precision.
If the result must be inexact, it is rounded using the round-half-even
algorithm.
If the result must be inexact, it is rounded using the
round-half-even algorithm.
>>> ExtendedContext.sqrt(Decimal('0'))
Decimal('0')
+69 -59
View File
@@ -83,27 +83,28 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
wrapped. (If a file descriptor is given, it is closed when the
returned I/O object is closed, unless closefd is set to False.)
mode is an optional string that specifies the mode in which the file is
opened. It defaults to 'r' which means open for reading in text mode. Other
common values are 'w' for writing (truncating the file if it already
exists), 'x' for exclusive creation of a new file, and 'a' for appending
(which on some Unix systems, means that all writes append to the end of the
file regardless of the current seek position). In text mode, if encoding is
not specified the encoding used is platform dependent. (For reading and
writing raw bytes use binary mode and leave encoding unspecified.) The
available modes are:
mode is an optional string that specifies the mode in which the file
is opened. It defaults to 'r' which means open for reading in text
mode. Other common values are 'w' for writing (truncating the file if
it already exists), 'x' for exclusive creation of a new file, and
'a' for appending (which on some Unix systems, means that all writes
append to the end of the file regardless of the current seek position).
In text mode, if encoding is not specified the encoding used is platform
dependent. (For reading and writing raw bytes use binary mode and leave
encoding unspecified.) The available modes are:
========= ===============================================================
========= ==========================================================
Character Meaning
--------- ---------------------------------------------------------------
--------- ----------------------------------------------------------
'r' open for reading (default)
'w' open for writing, truncating the file first
'x' create a new file and open it for writing
'a' open for writing, appending to the end of the file if it exists
'a' open for writing, appending to the end of the file if it
exists
'b' binary mode
't' text mode (default)
'+' open a disk file for updating (reading and writing)
========= ===============================================================
========= ==========================================================
The default mode is 'rt' (open for reading text). For binary random
access, the mode 'w+b' opens and truncates the file to 0 bytes, while
@@ -111,23 +112,23 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
raises an `FileExistsError` if the file already exists.
Python distinguishes between files opened in binary and text modes,
even when the underlying operating system doesn't. Files opened in
even when the underlying operating system doesn't. Files opened in
binary mode (appending 'b' to the mode argument) return contents as
bytes objects without any decoding. In text mode (the default, or when
bytes objects without any decoding. In text mode (the default, or when
't' is appended to the mode argument), the contents of the file are
returned as strings, the bytes having been first decoded using a
platform-dependent encoding or using the specified encoding if given.
buffering is an optional integer used to set the buffering policy.
Pass 0 to switch buffering off (only allowed in binary mode), 1 to select
line buffering (only usable in text mode), and an integer > 1 to indicate
the size of a fixed-size chunk buffer. When no buffering argument is
given, the default buffering policy works as follows:
Pass 0 to switch buffering off (only allowed in binary mode), 1 to
select line buffering (only usable in text mode), and an integer > 1 to
indicate the size of a fixed-size chunk buffer. When no buffering
argument is given, the default buffering policy works as follows:
* Binary files are buffered in fixed-size chunks; the size of the buffer
is max(min(blocksize, 8 MiB), DEFAULT_BUFFER_SIZE)
when the device block size is available.
On most systems, the buffer will typically be 128 kilobytes long.
* Binary files are buffered in fixed-size chunks; the size of the buffer
is max(min(blocksize, 8 MiB), DEFAULT_BUFFER_SIZE) when the device
block size is available.
On most systems, the buffer will typically be 128 kilobytes long.
* "Interactive" text files (files for which isatty() returns True)
use line buffering. Other text files use the policy described above
@@ -147,8 +148,8 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
encoding error strings.
newline is a string controlling how universal newlines works (it only
applies to text mode). It can be None, '', '\n', '\r', and '\r\n'. It works
as follows:
applies to text mode). It can be None, '', '\n', '\r', and '\r\n'. It
works as follows:
* On input, if newline is None, universal newlines mode is
enabled. Lines in the input can end in '\n', '\r', or '\r\n', and
@@ -164,17 +165,17 @@ def open(file, mode="r", buffering=-1, encoding=None, errors=None,
other legal values, any '\n' characters written are translated to
the given string.
closedfd is a bool. If closefd is False, the underlying file descriptor will
be kept open when the file is closed. This does not work when a file name is
given and must be True in that case.
closedfd is a bool. If closefd is False, the underlying file descriptor
will be kept open when the file is closed. This does not work when
a file name is given and must be True in that case.
The newly created file is non-inheritable.
A custom opener can be used by passing a callable as *opener*. The
underlying file descriptor for the file object is then obtained by calling
*opener* with (*file*, *flags*). *opener* must return an open file
descriptor (passing os.open as *opener* results in functionality similar to
passing None).
underlying file descriptor for the file object is then obtained by
calling *opener* with (*file*, *flags*). *opener* must return an open
file descriptor (passing os.open as *opener* results in functionality
similar to passing None).
open() returns a file object whose type depends on the mode, and
through which the standard file operations such as reading and writing
@@ -351,10 +352,12 @@ class IOBase(metaclass=abc.ABCMeta):
interpreted relative to the position indicated by whence. Values
for whence are ints:
* 0 -- start of stream (the default); offset should be zero or positive
* 0 -- start of stream (the default); offset should be zero or
positive
* 1 -- current stream position; offset may be negative
* 2 -- end of stream; offset is usually negative
Some operating systems / file systems could provide additional values.
Some operating systems / file systems could provide additional
values.
Return an int indicating the new absolute position.
"""
@@ -367,8 +370,8 @@ class IOBase(metaclass=abc.ABCMeta):
def truncate(self, pos=None):
"""Truncate file to size bytes.
Size defaults to the current IO position as reported by tell(). Return
the new size.
Size defaults to the current IO position as reported by tell().
Return the new size.
"""
self._unsupported("truncate")
@@ -492,7 +495,8 @@ class IOBase(metaclass=abc.ABCMeta):
def fileno(self):
"""Returns underlying file descriptor (an int) if one exists.
An OSError is raised if the IO object does not use a file descriptor.
An OSError is raised if the IO object does not use a file
descriptor.
"""
self._unsupported("fileno")
@@ -1485,17 +1489,22 @@ class FileIO(RawIOBase):
_closefd = True
def __init__(self, file, mode='r', closefd=True, opener=None):
"""Open a file. The mode can be 'r' (default), 'w', 'x' or 'a' for reading,
writing, exclusive creation or appending. The file will be created if it
doesn't exist when opened for writing or appending; it will be truncated
when opened for writing. A FileExistsError will be raised if it already
exists when opened for creating. Opening a file for creating implies
writing so this mode behaves in a similar way to 'w'. Add a '+' to the mode
to allow simultaneous reading and writing. A custom opener can be used by
passing a callable as *opener*. The underlying file descriptor for the file
object is then obtained by calling opener with (*name*, *flags*).
*opener* must return an open file descriptor (passing os.open as *opener*
results in functionality similar to passing None).
"""Open a file.
The mode can be 'r' (default), 'w', 'x' or 'a' for reading,
writing, exclusive creation or appending. The file will be created
if it doesn't exist when opened for writing or appending; it will be
truncated when opened for writing. A FileExistsError will be raised
if it already exists when opened for creating. Opening a file for
creating implies writing so this mode behaves in a similar way to
'w'. Add a '+' to the mode to allow simultaneous reading and
writing.
A custom opener can be used by passing a callable as *opener*.
The underlying file descriptor for the file object is then obtained
by calling opener with (*name*, *flags*). *opener* must return
an open file descriptor (passing os.open as *opener* results in
functionality similar to passing None).
"""
if self._fd >= 0:
# Have to close the existing file first.
@@ -1733,8 +1742,8 @@ class FileIO(RawIOBase):
"""Write bytes b to file, return number written.
Only makes one system call, so not all of the data may be written.
The number of bytes actually written is returned. In non-blocking mode,
returns None if the write would block.
The number of bytes actually written is returned. In non-blocking
mode, returns None if the write would block.
"""
self._checkClosed()
self._checkWritable()
@@ -1746,11 +1755,12 @@ class FileIO(RawIOBase):
def seek(self, pos, whence=SEEK_SET):
"""Move to new file position.
Argument offset is a byte count. Optional argument whence defaults to
SEEK_SET or 0 (offset from start of file, offset should be >= 0); other values
are SEEK_CUR or 1 (move relative to current position, positive or negative),
and SEEK_END or 2 (move relative to end of file, usually negative, although
many platforms allow seeking beyond the end of a file).
Argument offset is a byte count. Optional argument whence defaults
to SEEK_SET or 0 (offset from start of file, offset should be >= 0);
other values are SEEK_CUR or 1 (move relative to current position,
positive or negative), and SEEK_END or 2 (move relative to end of
file, usually negative, although many platforms allow seeking beyond
the end of a file).
Note that not all file objects are seekable.
"""
@@ -1783,8 +1793,8 @@ class FileIO(RawIOBase):
def close(self):
"""Close the file.
A closed file cannot be used for further I/O operations. close() may be
called more than once without error.
A closed file cannot be used for further I/O operations.
close() may be called more than once without error.
"""
if not self.closed:
self._stat_atopen = None
@@ -1879,8 +1889,8 @@ class TextIOBase(IOBase):
def read(self, size=-1):
"""Read at most size characters from stream, where size is an int.
Read from underlying buffer until we have size characters or we hit EOF.
If size is negative or omitted, read until EOF.
Read from underlying buffer until we have size characters or we hit
EOF. If size is negative or omitted, read until EOF.
Returns a string.
"""
+4
View File
@@ -1,6 +1,10 @@
# Important: don't add things to this module, as they will end up in the REPL's
# default globals. Use _pyrepl.main instead.
# Avoid caching this file by linecache and incorrectly report tracebacks.
# See https://github.com/python/cpython/issues/129098.
__spec__ = __loader__ = None
if __name__ == "__main__":
from .main import interactive_console as __pyrepl_interactive_console
__pyrepl_interactive_console()
-68
View File
@@ -1,68 +0,0 @@
"""Minimal '_curses' module, the low-level interface for curses module
which is not meant to be used directly.
Based on ctypes. It's too incomplete to be really called '_curses', so
to use it, you have to import it and stick it in sys.modules['_curses']
manually.
Note that there is also a built-in module _minimal_curses which will
hide this one if compiled in.
"""
import ctypes
import ctypes.util
class error(Exception):
pass
def _find_clib() -> str:
trylibs = ["ncursesw", "ncurses", "curses"]
for lib in trylibs:
path = ctypes.util.find_library(lib)
if path:
return path
raise ModuleNotFoundError("curses library not found", name="_pyrepl._minimal_curses")
_clibpath = _find_clib()
clib = ctypes.cdll.LoadLibrary(_clibpath)
clib.setupterm.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.POINTER(ctypes.c_int)]
clib.setupterm.restype = ctypes.c_int
clib.tigetstr.argtypes = [ctypes.c_char_p]
clib.tigetstr.restype = ctypes.c_ssize_t
clib.tparm.argtypes = [ctypes.c_char_p] + 9 * [ctypes.c_int] # type: ignore[operator]
clib.tparm.restype = ctypes.c_char_p
OK = 0
ERR = -1
# ____________________________________________________________
def setupterm(termstr, fd):
err = ctypes.c_int(0)
result = clib.setupterm(termstr, fd, ctypes.byref(err))
if result == ERR:
raise error("setupterm() failed (err=%d)" % err.value)
def tigetstr(cap):
if not isinstance(cap, bytes):
cap = cap.encode("ascii")
result = clib.tigetstr(cap)
if result == ERR:
return None
return ctypes.cast(result, ctypes.c_char_p).value
def tparm(str, i1=0, i2=0, i3=0, i4=0, i5=0, i6=0, i7=0, i8=0, i9=0):
result = clib.tparm(str, i1, i2, i3, i4, i5, i6, i7, i8, i9)
if result is None:
raise error("tparm() returned NULL")
return result
+426
View File
@@ -0,0 +1,426 @@
from __future__ import annotations
import importlib
import os
import pkgutil
import sys
import token
import tokenize
from importlib.machinery import FileFinder
from io import StringIO
from contextlib import contextmanager
from dataclasses import dataclass
from itertools import chain
from tokenize import TokenInfo
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any, Iterable, Iterator, Mapping
HARDCODED_SUBMODULES = {
# Standard library submodules that are not detected by pkgutil.iter_modules
# but can be imported, so should be proposed in completion
"collections": ["abc"],
"os": ["path"],
"xml.parsers.expat": ["errors", "model"],
}
def make_default_module_completer() -> ModuleCompleter:
# Inside pyrepl, __package__ is set to None by default
return ModuleCompleter(namespace={'__package__': None})
class ModuleCompleter:
"""A completer for Python import statements.
Examples:
- import <tab>
- import foo<tab>
- import foo.<tab>
- import foo as bar, baz<tab>
- from <tab>
- from foo<tab>
- from foo import <tab>
- from foo import bar<tab>
- from foo import (bar as baz, qux<tab>
"""
def __init__(self, namespace: Mapping[str, Any] | None = None) -> None:
self.namespace = namespace or {}
self._global_cache: list[pkgutil.ModuleInfo] = []
self._curr_sys_path: list[str] = sys.path[:]
self._stdlib_path = os.path.dirname(importlib.__path__[0])
def get_completions(self, line: str) -> list[str] | None:
"""Return the next possible import completions for 'line'."""
result = ImportParser(line).parse()
if not result:
return None
try:
return self.complete(*result)
except Exception:
# Some unexpected error occurred, make it look like
# no completions are available
return []
def complete(self, from_name: str | None, name: str | None) -> list[str]:
if from_name is None:
# import x.y.z<tab>
assert name is not None
path, prefix = self.get_path_and_prefix(name)
modules = self.find_modules(path, prefix)
return [self.format_completion(path, module) for module in modules]
if name is None:
# from x.y.z<tab>
path, prefix = self.get_path_and_prefix(from_name)
modules = self.find_modules(path, prefix)
return [self.format_completion(path, module) for module in modules]
# from x.y import z<tab>
return self.find_modules(from_name, name)
def find_modules(self, path: str, prefix: str) -> list[str]:
"""Find all modules under 'path' that start with 'prefix'."""
modules = self._find_modules(path, prefix)
# Filter out invalid module names
# (for example those containing dashes that cannot be imported with 'import')
return [mod for mod in modules if mod.isidentifier()]
def _find_modules(self, path: str, prefix: str) -> list[str]:
if not path:
# Top-level import (e.g. `import foo<tab>`` or `from foo<tab>`)`
builtin_modules = [name for name in sys.builtin_module_names
if self.is_suggestion_match(name, prefix)]
third_party_modules = [module.name for module in self.global_cache
if self.is_suggestion_match(module.name, prefix)]
return sorted(builtin_modules + third_party_modules)
if path.startswith('.'):
# Convert relative path to absolute path
package = self.namespace.get('__package__', '')
path = self.resolve_relative_name(path, package) # type: ignore[assignment]
if path is None:
return []
modules: Iterable[pkgutil.ModuleInfo] = self.global_cache
imported_module = sys.modules.get(path.split('.')[0])
if imported_module:
# Filter modules to those who name and specs match the
# imported module to avoid invalid suggestions
spec = imported_module.__spec__
if spec:
modules = [mod for mod in modules
if mod.name == spec.name
and mod.module_finder.find_spec(mod.name, None) == spec]
else:
modules = []
is_stdlib_import: bool | None = None
for segment in path.split('.'):
modules = [mod_info for mod_info in modules
if mod_info.ispkg and mod_info.name == segment]
if is_stdlib_import is None:
# Top-level import decide if we import from stdlib or not
is_stdlib_import = all(
self._is_stdlib_module(mod_info) for mod_info in modules
)
modules = self.iter_submodules(modules)
module_names = [module.name for module in modules]
if is_stdlib_import:
module_names.extend(HARDCODED_SUBMODULES.get(path, ()))
return [module_name for module_name in module_names
if self.is_suggestion_match(module_name, prefix)]
def _is_stdlib_module(self, module_info: pkgutil.ModuleInfo) -> bool:
return (isinstance(module_info.module_finder, FileFinder)
and module_info.module_finder.path == self._stdlib_path)
def is_suggestion_match(self, module_name: str, prefix: str) -> bool:
if prefix:
return module_name.startswith(prefix)
# For consistency with attribute completion, which
# does not suggest private attributes unless requested.
return not module_name.startswith("_")
def iter_submodules(self, parent_modules: list[pkgutil.ModuleInfo]) -> Iterator[pkgutil.ModuleInfo]:
"""Iterate over all submodules of the given parent modules."""
specs = [info.module_finder.find_spec(info.name, None)
for info in parent_modules if info.ispkg]
search_locations = set(chain.from_iterable(
getattr(spec, 'submodule_search_locations', [])
for spec in specs if spec
))
return pkgutil.iter_modules(search_locations)
def get_path_and_prefix(self, dotted_name: str) -> tuple[str, str]:
"""
Split a dotted name into an import path and a
final prefix that is to be completed.
Examples:
'foo.bar' -> 'foo', 'bar'
'foo.' -> 'foo', ''
'.foo' -> '.', 'foo'
"""
if '.' not in dotted_name:
return '', dotted_name
if dotted_name.startswith('.'):
stripped = dotted_name.lstrip('.')
dots = '.' * (len(dotted_name) - len(stripped))
if '.' not in stripped:
return dots, stripped
path, prefix = stripped.rsplit('.', 1)
return dots + path, prefix
path, prefix = dotted_name.rsplit('.', 1)
return path, prefix
def format_completion(self, path: str, module: str) -> str:
if path == '' or path.endswith('.'):
return f'{path}{module}'
return f'{path}.{module}'
def resolve_relative_name(self, name: str, package: str) -> str | None:
"""Resolve a relative module name to an absolute name.
Example: resolve_relative_name('.foo', 'bar') -> 'bar.foo'
"""
# taken from importlib._bootstrap
level = 0
for character in name:
if character != '.':
break
level += 1
bits = package.rsplit('.', level - 1)
if len(bits) < level:
return None
base = bits[0]
name = name[level:]
return f'{base}.{name}' if name else base
@property
def global_cache(self) -> list[pkgutil.ModuleInfo]:
"""Global module cache"""
if not self._global_cache or self._curr_sys_path != sys.path:
self._curr_sys_path = sys.path[:]
self._global_cache = list(pkgutil.iter_modules())
return self._global_cache
class ImportParser:
"""
Parses incomplete import statements that are
suitable for autocomplete suggestions.
Examples:
- import foo -> Result(from_name=None, name='foo')
- import foo. -> Result(from_name=None, name='foo.')
- from foo -> Result(from_name='foo', name=None)
- from foo import bar -> Result(from_name='foo', name='bar')
- from .foo import ( -> Result(from_name='.foo', name='')
Note that the parser works in reverse order, starting from the
last token in the input string. This makes the parser more robust
when parsing multiple statements.
"""
_ignored_tokens = {
token.INDENT, token.DEDENT, token.COMMENT,
token.NL, token.NEWLINE, token.ENDMARKER
}
_keywords = {'import', 'from', 'as'}
def __init__(self, code: str) -> None:
self.code = code
tokens = []
try:
for t in tokenize.generate_tokens(StringIO(code).readline):
if t.type not in self._ignored_tokens:
tokens.append(t)
except tokenize.TokenError as e:
if 'unexpected EOF' not in str(e):
# unexpected EOF is fine, since we're parsing an
# incomplete statement, but other errors are not
# because we may not have all the tokens so it's
# safer to bail out
tokens = []
except SyntaxError:
tokens = []
self.tokens = TokenQueue(tokens[::-1])
def parse(self) -> tuple[str | None, str | None] | None:
if not (res := self._parse()):
return None
return res.from_name, res.name
def _parse(self) -> Result | None:
with self.tokens.save_state():
return self.parse_from_import()
with self.tokens.save_state():
return self.parse_import()
def parse_import(self) -> Result:
if self.code.rstrip().endswith('import') and self.code.endswith(' '):
return Result(name='')
if self.tokens.peek_string(','):
name = ''
else:
if self.code.endswith(' '):
raise ParseError('parse_import')
name = self.parse_dotted_name()
if name.startswith('.'):
raise ParseError('parse_import')
while self.tokens.peek_string(','):
self.tokens.pop()
self.parse_dotted_as_name()
if self.tokens.peek_string('import'):
return Result(name=name)
raise ParseError('parse_import')
def parse_from_import(self) -> Result:
stripped = self.code.rstrip()
if stripped.endswith('import') and self.code.endswith(' '):
return Result(from_name=self.parse_empty_from_import(), name='')
if stripped.endswith('from') and self.code.endswith(' '):
return Result(from_name='')
if self.tokens.peek_string('(') or self.tokens.peek_string(','):
return Result(from_name=self.parse_empty_from_import(), name='')
if self.code.endswith(' '):
raise ParseError('parse_from_import')
name = self.parse_dotted_name()
if '.' in name:
self.tokens.pop_string('from')
return Result(from_name=name)
if self.tokens.peek_string('from'):
return Result(from_name=name)
from_name = self.parse_empty_from_import()
return Result(from_name=from_name, name=name)
def parse_empty_from_import(self) -> str:
if self.tokens.peek_string(','):
self.tokens.pop()
self.parse_as_names()
if self.tokens.peek_string('('):
self.tokens.pop()
self.tokens.pop_string('import')
return self.parse_from()
def parse_from(self) -> str:
from_name = self.parse_dotted_name()
self.tokens.pop_string('from')
return from_name
def parse_dotted_as_name(self) -> str:
self.tokens.pop_name()
if self.tokens.peek_string('as'):
self.tokens.pop()
with self.tokens.save_state():
return self.parse_dotted_name()
def parse_dotted_name(self) -> str:
name = []
if self.tokens.peek_string('.'):
name.append('.')
self.tokens.pop()
if (self.tokens.peek_name()
and (tok := self.tokens.peek())
and tok.string not in self._keywords):
name.append(self.tokens.pop_name())
if not name:
raise ParseError('parse_dotted_name')
while self.tokens.peek_string('.'):
name.append('.')
self.tokens.pop()
if (self.tokens.peek_name()
and (tok := self.tokens.peek())
and tok.string not in self._keywords):
name.append(self.tokens.pop_name())
else:
break
while self.tokens.peek_string('.'):
name.append('.')
self.tokens.pop()
return ''.join(name[::-1])
def parse_as_names(self) -> None:
self.parse_as_name()
while self.tokens.peek_string(','):
self.tokens.pop()
self.parse_as_name()
def parse_as_name(self) -> None:
self.tokens.pop_name()
if self.tokens.peek_string('as'):
self.tokens.pop()
self.tokens.pop_name()
class ParseError(Exception):
pass
@dataclass(frozen=True)
class Result:
from_name: str | None = None
name: str | None = None
class TokenQueue:
"""Provides helper functions for working with a sequence of tokens."""
def __init__(self, tokens: list[TokenInfo]) -> None:
self.tokens: list[TokenInfo] = tokens
self.index: int = 0
self.stack: list[int] = []
@contextmanager
def save_state(self) -> Any:
try:
self.stack.append(self.index)
yield
except ParseError:
self.index = self.stack.pop()
else:
self.stack.pop()
def __bool__(self) -> bool:
return self.index < len(self.tokens)
def peek(self) -> TokenInfo | None:
if not self:
return None
return self.tokens[self.index]
def peek_name(self) -> bool:
if not (tok := self.peek()):
return False
return tok.type == token.NAME
def pop_name(self) -> str:
tok = self.pop()
if tok.type != token.NAME:
raise ParseError('pop_name')
return tok.string
def peek_string(self, string: str) -> bool:
if not (tok := self.peek()):
return False
return tok.string == string
def pop_string(self, string: str) -> str:
tok = self.pop()
if tok.string != string:
raise ParseError('pop_string')
return tok.string
def pop(self) -> TokenInfo:
if not self:
raise ParseError('pop')
tok = self.tokens[self.index]
self.index += 1
return tok
+110
View File
@@ -0,0 +1,110 @@
# Copyright 2000-2008 Michael Hudson-Doyle <micahel@gmail.com>
# Armin Rigo
#
# All Rights Reserved
#
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose is hereby granted without fee,
# provided that the above copyright notice appear in all copies and
# that both that copyright notice and this permission notice appear in
# supporting documentation.
#
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
OS-independent base for an event and VT sequence scanner
See unix_eventqueue and windows_eventqueue for subclasses.
"""
from collections import deque
from . import keymap
from .console import Event
from .trace import trace
class BaseEventQueue:
def __init__(self, encoding: str, keymap_dict: dict[bytes, str]) -> None:
self.compiled_keymap = keymap.compile_keymap(keymap_dict)
self.keymap = self.compiled_keymap
trace("keymap {k!r}", k=self.keymap)
self.encoding = encoding
self.events: deque[Event] = deque()
self.buf = bytearray()
def get(self) -> Event | None:
"""
Retrieves the next event from the queue.
"""
if self.events:
return self.events.popleft()
else:
return None
def empty(self) -> bool:
"""
Checks if the queue is empty.
"""
return not self.events
def flush_buf(self) -> bytearray:
"""
Flushes the buffer and returns its contents.
"""
old = self.buf
self.buf = bytearray()
return old
def insert(self, event: Event) -> None:
"""
Inserts an event into the queue.
"""
trace('added event {event}', event=event)
self.events.append(event)
def push(self, char: int | bytes) -> None:
"""
Processes a character by updating the buffer and handling special key mappings.
"""
assert isinstance(char, (int, bytes))
ord_char = char if isinstance(char, int) else ord(char)
char = ord_char.to_bytes()
self.buf.append(ord_char)
if char in self.keymap:
if self.keymap is self.compiled_keymap:
# sanity check, buffer is empty when a special key comes
assert len(self.buf) == 1
k = self.keymap[char]
trace('found map {k!r}', k=k)
if isinstance(k, dict):
self.keymap = k
else:
self.insert(Event('key', k, bytes(self.flush_buf())))
self.keymap = self.compiled_keymap
elif self.buf and self.buf[0] == 27: # escape
# escape sequence not recognized by our keymap: propagate it
# outside so that i can be recognized as an M-... key (see also
# the docstring in keymap.py
trace('unrecognized escape sequence, propagating...')
self.keymap = self.compiled_keymap
self.insert(Event('key', '\033', b'\033'))
for _c in self.flush_buf()[1:]:
self.push(_c)
else:
try:
decoded = bytes(self.buf).decode(self.encoding)
except UnicodeError:
return
else:
self.insert(Event('key', decoded, bytes(self.flush_buf())))
self.keymap = self.compiled_keymap
+36 -20
View File
@@ -21,6 +21,7 @@
from __future__ import annotations
import os
import time
# Categories of actions:
# killing
@@ -31,6 +32,7 @@ import os
# finishing
# [completion]
from .trace import trace
# types
if False:
@@ -368,6 +370,13 @@ class self_insert(EditCommand):
r = self.reader
text = self.event * r.get_arg()
r.insert(text)
if r.paste_mode:
data = ""
ev = r.console.getpending()
data += ev.data
if data:
r.insert(data)
r.last_refresh_cache.invalidated = True
class insert_nl(EditCommand):
@@ -411,14 +420,17 @@ class delete(EditCommand):
def do(self) -> None:
r = self.reader
b = r.buffer
if (
r.pos == 0
and len(b) == 0 # this is something of a hack
and self.event[-1] == "\004"
):
r.update_screen()
r.console.finish()
raise EOFError
if self.event[-1] == "\004":
if b and b[-1].endswith("\n"):
self.finish = True
elif (
r.pos == 0
and len(b) == 0 # this is something of a hack
):
r.update_screen()
r.console.finish()
raise EOFError
for i in range(r.get_arg()):
if r.pos != len(b):
del b[r.pos]
@@ -437,7 +449,7 @@ class help(Command):
import _sitebuiltins
with self.reader.suspend():
self.reader.msg = _sitebuiltins._Helper()() # type: ignore[assignment, call-arg]
self.reader.msg = _sitebuiltins._Helper()() # type: ignore[assignment]
class invalid_key(Command):
@@ -456,7 +468,7 @@ class invalid_command(Command):
class show_history(Command):
def do(self) -> None:
from .pager import get_pager
from site import gethistoryfile # type: ignore[attr-defined]
from site import gethistoryfile
history = os.linesep.join(self.reader.history[:])
self.reader.console.restore()
@@ -471,19 +483,23 @@ class show_history(Command):
class paste_mode(Command):
def do(self) -> None:
self.reader.paste_mode = not self.reader.paste_mode
self.reader.dirty = True
class enable_bracketed_paste(Command):
class perform_bracketed_paste(Command):
def do(self) -> None:
self.reader.paste_mode = True
self.reader.in_bracketed_paste = True
class disable_bracketed_paste(Command):
def do(self) -> None:
self.reader.paste_mode = False
self.reader.in_bracketed_paste = False
self.reader.dirty = True
done = "\x1b[201~"
data = ""
start = time.time()
while done not in data:
ev = self.reader.console.getpending()
data += ev.data
trace(
"bracketed pasting of {l} chars done in {s:.2f}s",
l=len(data),
s=time.time() - start,
)
self.reader.insert(data.replace(done, ""))
self.reader.last_refresh_cache.invalidated = True
+5 -1
View File
@@ -91,7 +91,7 @@ def build_menu(
# D E F B E
# G C F
#
# "fill" the table with empty words, so we always have the same amout
# "fill" the table with empty words, so we always have the same amount
# of rows for each column
missing = cols*rows - len(wordlist)
wordlist = wordlist + ['']*missing
@@ -293,3 +293,7 @@ class CompletingReader(Reader):
def get_completions(self, stem: str) -> list[str]:
return []
def get_line(self) -> str:
"""Return the current line until the cursor position."""
return ''.join(self.buffer[:self.pos])
+19 -3
View File
@@ -19,11 +19,12 @@
from __future__ import annotations
import _colorize # type: ignore[import-not-found]
import _colorize
from abc import ABC, abstractmethod
import ast
import code
import linecache
from dataclasses import dataclass, field
import os.path
import sys
@@ -152,6 +153,8 @@ class Console(ABC):
class InteractiveColoredConsole(code.InteractiveConsole):
STATEMENT_FAILED = object()
def __init__(
self,
locals: dict[str, object] | None = None,
@@ -159,7 +162,7 @@ class InteractiveColoredConsole(code.InteractiveConsole):
*,
local_exit: bool = False,
) -> None:
super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg]
super().__init__(locals=locals, filename=filename, local_exit=local_exit)
self.can_colorize = _colorize.can_colorize()
def showsyntaxerror(self, filename=None, **kwargs):
@@ -173,6 +176,16 @@ class InteractiveColoredConsole(code.InteractiveConsole):
limit=traceback.BUILTIN_EXCEPTION_LIMIT)
self.write(''.join(lines))
def runcode(self, code):
try:
exec(code, self.locals)
except SystemExit:
raise
except BaseException:
self.showtraceback()
return self.STATEMENT_FAILED
return None
def runsource(self, source, filename="<input>", symbol="single"):
try:
tree = self.compile.compiler(
@@ -193,6 +206,7 @@ class InteractiveColoredConsole(code.InteractiveConsole):
item = wrapper([stmt])
try:
code = self.compile.compiler(item, filename, the_symbol)
linecache._register_code(code, source, filename)
except SyntaxError as e:
if e.args[0] == "'await' outside function":
python = os.path.basename(sys.executable)
@@ -209,5 +223,7 @@ class InteractiveColoredConsole(code.InteractiveConsole):
if code is None:
return True
self.runcode(code)
result = self.runcode(code)
if result is self.STATEMENT_FAILED:
break
return False
-33
View File
@@ -1,33 +0,0 @@
# Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
# Armin Rigo
#
# All Rights Reserved
#
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose is hereby granted without fee,
# provided that the above copyright notice appear in all copies and
# that both that copyright notice and this permission notice appear in
# supporting documentation.
#
# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
try:
import _curses
except ImportError:
try:
import curses as _curses # type: ignore[no-redef]
except ImportError:
from . import _minimal_curses as _curses # type: ignore[no-redef]
setupterm = _curses.setupterm
tigetstr = _curses.tigetstr
tparm = _curses.tparm
error = _curses.error
+26 -20
View File
@@ -20,19 +20,25 @@
import termios
class TermState:
def __init__(self, tuples):
(
self.iflag,
self.oflag,
self.cflag,
self.lflag,
self.ispeed,
self.ospeed,
self.cc,
) = tuples
TYPE_CHECKING = False
def as_list(self):
if TYPE_CHECKING:
from typing import cast
else:
cast = lambda typ, val: val
class TermState:
def __init__(self, attrs: list[int | list[bytes]]) -> None:
self.iflag = cast(int, attrs[0])
self.oflag = cast(int, attrs[1])
self.cflag = cast(int, attrs[2])
self.lflag = cast(int, attrs[3])
self.ispeed = cast(int, attrs[4])
self.ospeed = cast(int, attrs[5])
self.cc = cast(list[bytes], attrs[6])
def as_list(self) -> list[int | list[bytes]]:
return [
self.iflag,
self.oflag,
@@ -45,32 +51,32 @@ class TermState:
self.cc[:],
]
def copy(self):
def copy(self) -> "TermState":
return self.__class__(self.as_list())
def tcgetattr(fd):
def tcgetattr(fd: int) -> TermState:
return TermState(termios.tcgetattr(fd))
def tcsetattr(fd, when, attrs):
def tcsetattr(fd: int, when: int, attrs: TermState) -> None:
termios.tcsetattr(fd, when, attrs.as_list())
class Term(TermState):
TS__init__ = TermState.__init__
def __init__(self, fd=0):
def __init__(self, fd: int = 0) -> None:
self.TS__init__(termios.tcgetattr(fd))
self.fd = fd
self.stack = []
self.stack: list[list[int | list[bytes]]] = []
def save(self):
def save(self) -> None:
self.stack.append(self.as_list())
def set(self, when=termios.TCSANOW):
def set(self, when: int = termios.TCSANOW) -> None:
termios.tcsetattr(self.fd, when, self.as_list())
def restore(self):
def restore(self) -> None:
self.TS__init__(self.stack.pop())
self.set()
+9 -9
View File
@@ -30,20 +30,20 @@ sequence.
pyrepl uses its own keyspec format that is meant to be a strict superset of
readline's KEYSEQ format. This means that if a spec is found that readline
accepts that this doesn't, it should be logged as a bug. Note that this means
we're using the `\\C-o' style of readline's keyspec, not the `Control-o' sort.
we're using the '\\C-o' style of readline's keyspec, not the 'Control-o' sort.
The extension to readline is that the sequence \\<KEY> denotes the
sequence of characters produced by hitting KEY.
Examples:
`a' - what you get when you hit the `a' key
`\\EOA' - Escape - O - A (up, on my terminal)
`\\<UP>' - the up arrow key
`\\<up>' - ditto (keynames are case-insensitive)
`\\C-o', `\\c-o' - control-o
`\\M-.' - meta-period
`\\E.' - ditto (that's how meta works for pyrepl)
`\\<tab>', `\\<TAB>', `\\t', `\\011', '\\x09', '\\X09', '\\C-i', '\\C-I'
'a' - what you get when you hit the 'a' key
'\\EOA' - Escape - O - A (up, on my terminal)
'\\<UP>' - the up arrow key
'\\<up>' - ditto (keynames are case-insensitive)
'\\C-o', '\\c-o' - control-o
'\\M-.' - meta-period
'\\E.' - ditto (that's how meta works for pyrepl)
'\\<tab>', '\\<TAB>', '\\t', '\\011', '\\x09', '\\X09', '\\C-i', '\\C-I'
- all of these are the tab character.
"""
+5 -6
View File
@@ -1,6 +1,7 @@
import errno
import os
import sys
import types
CAN_USE_PYREPL: bool
@@ -29,12 +30,10 @@ def interactive_console(mainmodule=None, quiet=False, pythonstartup=False):
print(FAIL_REASON, file=sys.stderr)
return sys._baserepl()
if mainmodule:
namespace = mainmodule.__dict__
else:
import __main__
namespace = __main__.__dict__
namespace.pop("__pyrepl_interactive_console", None)
if not mainmodule:
mainmodule = types.ModuleType("__main__")
namespace = mainmodule.__dict__
# sys._baserepl() above does this internally, we do it here
startup_path = os.getenv("PYTHONSTARTUP")
+2 -1
View File
@@ -4,8 +4,9 @@
[mypy]
files = Lib/_pyrepl
mypy_path = $MYPY_CONFIG_FILE_DIR/../../Misc/mypy
explicit_package_bases = True
python_version = 3.12
python_version = 3.13
platform = linux
pretty = True
+91 -134
View File
@@ -22,15 +22,13 @@
from __future__ import annotations
import sys
import _colorize
from contextlib import contextmanager
from dataclasses import dataclass, field, fields
import unicodedata
from _colorize import can_colorize, ANSIColors # type: ignore[import-not-found]
from . import commands, console, input
from .utils import ANSI_ESCAPE_SEQUENCE, wlen, str_width
from .utils import wlen, unbracket, disp_str, gen_colors, THEME
from .trace import trace
@@ -39,38 +37,7 @@ Command = commands.Command
from .types import Callback, SimpleContextManager, KeySpec, CommandName
def disp_str(buffer: str) -> tuple[str, list[int]]:
"""disp_str(buffer:string) -> (string, [int])
Return the string that should be the printed representation of
|buffer| and a list detailing where the characters of |buffer|
get used up. E.g.:
>>> disp_str(chr(3))
('^C', [1, 0])
"""
b: list[int] = []
s: list[str] = []
for c in buffer:
if c == '\x1a':
s.append(c)
b.append(2)
elif ord(c) < 128:
s.append(c)
b.append(1)
elif unicodedata.category(c).startswith("C"):
c = r"\u%04x" % ord(c)
s.append(c)
b.extend([0] * (len(c) - 1))
else:
s.append(c)
b.append(str_width(c))
return "".join(s), b
# syntax classes:
# syntax classes
SYNTAX_WHITESPACE, SYNTAX_WORD, SYNTAX_SYMBOL = range(3)
@@ -136,8 +103,7 @@ default_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple(
(r"\M-9", "digit-arg"),
(r"\M-\n", "accept"),
("\\\\", "self-insert"),
(r"\x1b[200~", "enable_bracketed_paste"),
(r"\x1b[201~", "disable_bracketed_paste"),
(r"\x1b[200~", "perform-bracketed-paste"),
(r"\x03", "ctrl-c"),
]
+ [(c, "self-insert") for c in map(chr, range(32, 127)) if c != "\\"]
@@ -175,20 +141,21 @@ class Reader:
Instance variables of note include:
* buffer:
A *list* (*not* a string at the moment :-) containing all the
characters that have been entered.
A per-character list containing all the characters that have been
entered. Does not include color information.
* console:
Hopefully encapsulates the OS dependent stuff.
* pos:
A 0-based index into `buffer' for where the insertion point
A 0-based index into 'buffer' for where the insertion point
is.
* screeninfo:
Ahem. This list contains some info needed to move the
insertion point around reasonably efficiently.
A list of screen position tuples. Each list element is a tuple
representing information on visible line length for a given line.
Allows for efficient skipping of color escape sequences.
* cxy, lxy:
the position of the insertion point in screen ...
* syntax_table:
Dictionary mapping characters to `syntax class'; read the
Dictionary mapping characters to 'syntax class'; read the
emacs docs to see what this means :-)
* commands:
Dictionary mapping command names to command classes.
@@ -234,7 +201,6 @@ class Reader:
dirty: bool = False
finished: bool = False
paste_mode: bool = False
in_bracketed_paste: bool = False
commands: dict[str, type[Command]] = field(default_factory=make_default_commands)
last_command: type[Command] | None = None
syntax_table: dict[str, int] = field(default_factory=make_default_syntax_table)
@@ -252,7 +218,6 @@ class Reader:
## cached metadata to speed up screen refreshes
@dataclass
class RefreshCache:
in_bracketed_paste: bool = False
screen: list[str] = field(default_factory=list)
screeninfo: list[tuple[int, list[int]]] = field(init=False)
line_end_offsets: list[int] = field(default_factory=list)
@@ -266,7 +231,6 @@ class Reader:
screen: list[str],
screeninfo: list[tuple[int, list[int]]],
) -> None:
self.in_bracketed_paste = reader.in_bracketed_paste
self.screen = screen.copy()
self.screeninfo = screeninfo.copy()
self.pos = reader.pos
@@ -279,8 +243,7 @@ class Reader:
return False
dimensions = reader.console.width, reader.console.height
dimensions_changed = dimensions != self.dimensions
paste_changed = reader.in_bracketed_paste != self.in_bracketed_paste
return not (dimensions_changed or paste_changed)
return not dimensions_changed
def get_cached_location(self, reader: Reader) -> tuple[int, int]:
if self.invalidated:
@@ -310,7 +273,7 @@ class Reader:
self.screeninfo = [(0, [])]
self.cxy = self.pos2xy()
self.lxy = (self.pos, 0)
self.can_colorize = can_colorize()
self.can_colorize = _colorize.can_colorize()
self.last_refresh_cache.screeninfo = self.screeninfo
self.last_refresh_cache.pos = self.pos
@@ -348,13 +311,17 @@ class Reader:
prompt_from_cache = (offset and self.buffer[offset - 1] != "\n")
if self.can_colorize:
colors = list(gen_colors(self.get_unicode()))
else:
colors = None
trace("colors = {colors}", colors=colors)
lines = "".join(self.buffer[offset:]).split("\n")
cursor_found = False
lines_beyond_cursor = 0
for ln, line in enumerate(lines, num_common_lines):
ll = len(line)
if 0 <= pos <= ll:
line_len = len(line)
if 0 <= pos <= line_len:
self.lxy = pos, ln
cursor_found = True
elif cursor_found:
@@ -368,34 +335,33 @@ class Reader:
prompt_from_cache = False
prompt = ""
else:
prompt = self.get_prompt(ln, ll >= pos >= 0)
prompt = self.get_prompt(ln, line_len >= pos >= 0)
while "\n" in prompt:
pre_prompt, _, prompt = prompt.partition("\n")
last_refresh_line_end_offsets.append(offset)
screen.append(pre_prompt)
screeninfo.append((0, []))
pos -= ll + 1
prompt, lp = self.process_prompt(prompt)
l, l2 = disp_str(line)
wrapcount = (wlen(l) + lp) // self.console.width
if wrapcount == 0:
offset += ll + 1 # Takes all of the line plus the newline
pos -= line_len + 1
prompt, prompt_len = self.process_prompt(prompt)
chars, char_widths = disp_str(line, colors, offset)
wrapcount = (sum(char_widths) + prompt_len) // self.console.width
if wrapcount == 0 or not char_widths:
offset += line_len + 1 # Takes all of the line plus the newline
last_refresh_line_end_offsets.append(offset)
screen.append(prompt + l)
screeninfo.append((lp, l2))
screen.append(prompt + "".join(chars))
screeninfo.append((prompt_len, char_widths))
else:
i = 0
while l:
prelen = lp if i == 0 else 0
pre = prompt
prelen = prompt_len
for wrap in range(wrapcount + 1):
index_to_wrap_before = 0
column = 0
for character_width in l2:
if column + character_width >= self.console.width - prelen:
for char_width in char_widths:
if column + char_width + prelen >= self.console.width:
break
index_to_wrap_before += 1
column += character_width
pre = prompt if i == 0 else ""
if len(l) > index_to_wrap_before:
column += char_width
if len(chars) > index_to_wrap_before:
offset += index_to_wrap_before
post = "\\"
after = [1]
@@ -404,11 +370,14 @@ class Reader:
post = ""
after = []
last_refresh_line_end_offsets.append(offset)
screen.append(pre + l[:index_to_wrap_before] + post)
screeninfo.append((prelen, l2[:index_to_wrap_before] + after))
l = l[index_to_wrap_before:]
l2 = l2[index_to_wrap_before:]
i += 1
render = pre + "".join(chars[:index_to_wrap_before]) + post
render_widths = char_widths[:index_to_wrap_before] + after
screen.append(render)
screeninfo.append((prelen, render_widths))
chars = chars[index_to_wrap_before:]
char_widths = char_widths[index_to_wrap_before:]
pre = ""
prelen = 0
self.screeninfo = screeninfo
self.cxy = self.pos2xy()
if self.msg:
@@ -421,42 +390,15 @@ class Reader:
@staticmethod
def process_prompt(prompt: str) -> tuple[str, int]:
"""Process the prompt.
r"""Return a tuple with the prompt string and its visible length.
This means calculate the length of the prompt. The character \x01
and \x02 are used to bracket ANSI control sequences and need to be
excluded from the length calculation. So also a copy of the prompt
is returned with these control characters removed."""
# The logic below also ignores the length of common escape
# sequences if they were not explicitly within \x01...\x02.
# They are CSI (or ANSI) sequences ( ESC [ ... LETTER )
# wlen from utils already excludes ANSI_ESCAPE_SEQUENCE chars,
# which breaks the logic below so we redefine it here.
def wlen(s: str) -> int:
return sum(str_width(i) for i in s)
out_prompt = ""
l = wlen(prompt)
pos = 0
while True:
s = prompt.find("\x01", pos)
if s == -1:
break
e = prompt.find("\x02", s)
if e == -1:
break
# Found start and end brackets, subtract from string length
l = l - (e - s + 1)
keep = prompt[pos:s]
l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep)))
out_prompt += keep + prompt[s + 1 : e]
pos = e + 1
keep = prompt[pos:]
l -= sum(map(wlen, ANSI_ESCAPE_SEQUENCE.findall(keep)))
out_prompt += keep
return out_prompt, l
The prompt string has the zero-width brackets recognized by shells
(\x01 and \x02) removed. The length ignores anything between those
brackets as well as any ANSI escape sequences.
"""
out_prompt = unbracket(prompt, including_content=False)
visible_prompt = unbracket(prompt, including_content=True)
return out_prompt, wlen(visible_prompt)
def bow(self, p: int | None = None) -> int:
"""Return the 0-based index of the word break preceding p most
@@ -525,7 +467,7 @@ class Reader:
def get_arg(self, default: int = 1) -> int:
"""Return any prefix argument that the user has supplied,
returning `default' if there is None. Defaults to 1.
returning 'default' if there is None. Defaults to 1.
"""
if self.arg is None:
return default
@@ -533,10 +475,10 @@ class Reader:
def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
"""Return what should be in the left-hand margin for line
`lineno'."""
'lineno'."""
if self.arg is not None and cursor_on_line:
prompt = f"(arg: {self.arg}) "
elif self.paste_mode and not self.in_bracketed_paste:
elif self.paste_mode:
prompt = "(paste) "
elif "\n" in self.buffer:
if lineno == 0:
@@ -549,7 +491,8 @@ class Reader:
prompt = self.ps1
if self.can_colorize:
prompt = f"{ANSIColors.BOLD_MAGENTA}{prompt}{ANSIColors.RESET}"
t = THEME()
prompt = f"{t.prompt}{prompt}{t.reset}"
return prompt
def push_input_trans(self, itrans: input.KeymapTranslator) -> None:
@@ -564,9 +507,9 @@ class Reader:
pos = 0
i = 0
while i < y:
prompt_len, character_widths = self.screeninfo[i]
offset = len(character_widths) - character_widths.count(0)
in_wrapped_line = prompt_len + sum(character_widths) >= self.console.width
prompt_len, char_widths = self.screeninfo[i]
offset = len(char_widths)
in_wrapped_line = prompt_len + sum(char_widths) >= self.console.width
if in_wrapped_line:
pos += offset - 1 # -1 cause backslash is not in buffer
else:
@@ -577,6 +520,7 @@ class Reader:
cur_x = self.screeninfo[i][0]
while cur_x < x:
if self.screeninfo[i][1][j] == 0:
j += 1 # prevent potential future infinite loop
continue
cur_x += self.screeninfo[i][1][j]
j += 1
@@ -586,29 +530,33 @@ class Reader:
def pos2xy(self) -> tuple[int, int]:
"""Return the x, y coordinates of position 'pos'."""
# this *is* incomprehensible, yes.
p, y = 0, 0
l2: list[int] = []
prompt_len, y = 0, 0
char_widths: list[int] = []
pos = self.pos
assert 0 <= pos <= len(self.buffer)
# optimize for the common case: typing at the end of the buffer
if pos == len(self.buffer) and len(self.screeninfo) > 0:
y = len(self.screeninfo) - 1
p, l2 = self.screeninfo[y]
return p + sum(l2) + l2.count(0), y
prompt_len, char_widths = self.screeninfo[y]
return prompt_len + sum(char_widths), y
for prompt_len, char_widths in self.screeninfo:
offset = len(char_widths)
in_wrapped_line = prompt_len + sum(char_widths) >= self.console.width
if in_wrapped_line:
offset -= 1 # need to remove line-wrapping backslash
for p, l2 in self.screeninfo:
l = len(l2) - l2.count(0)
in_wrapped_line = p + sum(l2) >= self.console.width
offset = l - 1 if in_wrapped_line else l # need to remove backslash
if offset >= pos:
break
if p + sum(l2) >= self.console.width:
pos -= l - 1 # -1 cause backslash is not in buffer
else:
pos -= l + 1 # +1 cause newline is in buffer
if not in_wrapped_line:
offset += 1 # there's a newline in buffer
pos -= offset
y += 1
return p + sum(l2[:pos]), y
return prompt_len + sum(char_widths[:pos]), y
def insert(self, text: str | list[str]) -> None:
"""Insert 'text' at the insertion point."""
@@ -619,6 +567,7 @@ class Reader:
def update_cursor(self) -> None:
"""Move the cursor to reflect changes in self.pos"""
self.cxy = self.pos2xy()
trace("update_cursor({pos}) = {cxy}", pos=self.pos, cxy=self.cxy)
self.console.move_cursor(*self.cxy)
def after_command(self, cmd: Command) -> None:
@@ -670,6 +619,16 @@ class Reader:
setattr(self, arg, prev_state[arg])
self.prepare()
@contextmanager
def suspend_colorization(self) -> SimpleContextManager:
try:
old_can_colorize = self.can_colorize
self.can_colorize = False
yield
finally:
self.can_colorize = old_can_colorize
def finish(self) -> None:
"""Called when a command signals that we're finished."""
pass
@@ -685,9 +644,7 @@ class Reader:
def refresh(self) -> None:
"""Recalculate and refresh the screen."""
if self.in_bracketed_paste and self.buffer and not self.buffer[-1] == "\n":
return
self.console.height, self.console.width = self.console.getheightwidth()
# this call sets up self.cxy, so call it first.
self.screen = self.calc_screen()
self.console.refresh(self.screen, self.cxy)
+31 -9
View File
@@ -32,20 +32,22 @@ import warnings
from dataclasses import dataclass, field
import os
from site import gethistoryfile # type: ignore[attr-defined]
from site import gethistoryfile
import sys
from rlcompleter import Completer as RLCompleter
from . import commands, historical_reader
from .completing_reader import CompletingReader
from .console import Console as ConsoleType
from ._module_completer import ModuleCompleter, make_default_module_completer
Console: type[ConsoleType]
_error: tuple[type[Exception], ...] | type[Exception]
try:
from .unix_console import UnixConsole as Console, _error
except ImportError:
if os.name == "nt":
from .windows_console import WindowsConsole as Console, _error
else:
from .unix_console import UnixConsole as Console, _error
ENCODING = sys.getdefaultencoding() or "latin1"
@@ -89,6 +91,7 @@ __all__ = [
# "set_pre_input_hook",
"set_startup_hook",
"write_history_file",
"append_history_file",
# ---- multiline extensions ----
"multiline_input",
]
@@ -99,7 +102,7 @@ __all__ = [
class ReadlineConfig:
readline_completer: Completer | None = None
completer_delims: frozenset[str] = frozenset(" \t\n`~!@#$%^&*()-=+[{]}\\|;:'\",<>/?")
module_completer: ModuleCompleter = field(default_factory=make_default_module_completer)
@dataclass(kw_only=True)
class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
@@ -132,6 +135,9 @@ class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
return "".join(b[p + 1 : self.pos])
def get_completions(self, stem: str) -> list[str]:
module_completions = self.get_module_completions()
if module_completions is not None:
return module_completions
if len(stem) == 0 and self.more_lines is not None:
b = self.buffer
p = self.pos
@@ -161,6 +167,10 @@ class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
result.sort()
return result
def get_module_completions(self) -> list[str] | None:
line = self.get_line()
return self.config.module_completer.get_completions(line)
def get_trimmed_history(self, maxlength: int) -> list[str]:
if maxlength >= 0:
cut = len(self.history) - maxlength
@@ -268,10 +278,6 @@ class maybe_accept(commands.Command):
r = self.reader # type: ignore[assignment]
r.dirty = True # this is needed to hide the completion menu, if visible
if self.reader.in_bracketed_paste:
r.insert("\n")
return
# if there are already several lines and the cursor
# is not on the last one, always insert a new \n.
text = r.get_unicode()
@@ -446,6 +452,7 @@ class _ReadlineWrapper:
del buffer[:]
if line:
history.append(line)
self.set_history_length(self.get_current_history_length())
def write_history_file(self, filename: str = gethistoryfile()) -> None:
maxlength = self.saved_history_length
@@ -457,6 +464,19 @@ class _ReadlineWrapper:
entry = entry.replace("\n", "\r\n") # multiline history support
f.write(entry + "\n")
def append_history_file(self, filename: str = gethistoryfile()) -> None:
reader = self.get_reader()
saved_length = self.get_history_length()
length = self.get_current_history_length() - saved_length
history = reader.get_trimmed_history(length)
f = open(os.path.expanduser(filename), "a",
encoding="utf-8", newline="\n")
with f:
for entry in history:
entry = entry.replace("\n", "\r\n") # multiline history support
f.write(entry + "\n")
self.set_history_length(saved_length + length)
def clear_history(self) -> None:
del self.get_reader().history[:]
@@ -526,6 +546,7 @@ set_history_length = _wrapper.set_history_length
get_current_history_length = _wrapper.get_current_history_length
read_history_file = _wrapper.read_history_file
write_history_file = _wrapper.write_history_file
append_history_file = _wrapper.append_history_file
clear_history = _wrapper.clear_history
get_history_item = _wrapper.get_history_item
remove_history_item = _wrapper.remove_history_item
@@ -587,6 +608,7 @@ def _setup(namespace: Mapping[str, Any]) -> None:
# set up namespace in rlcompleter, which requires it to be a bona fide dict
if not isinstance(namespace, dict):
namespace = dict(namespace)
_wrapper.config.module_completer = ModuleCompleter(namespace)
_wrapper.config.readline_completer = RLCompleter(namespace).complete
# this is not really what readline.c does. Better than nothing I guess
+25 -11
View File
@@ -26,18 +26,14 @@ allowing multiline input and multiline history entries.
from __future__ import annotations
import _sitebuiltins
import linecache
import functools
import os
import sys
import code
import warnings
import errno
from .readline import _get_reader, multiline_input
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any
from .readline import _get_reader, multiline_input, append_history_file
_error: tuple[type[Exception], ...] | type[Exception]
@@ -115,6 +111,10 @@ def run_multiline_interactive_console(
more_lines = functools.partial(_more_lines, console)
input_n = 0
_is_x_showrefcount_set = sys._xoptions.get("showrefcount")
_is_pydebug_build = hasattr(sys, "gettotalrefcount")
show_ref_count = _is_x_showrefcount_set and _is_pydebug_build
def maybe_run_command(statement: str) -> bool:
statement = statement.strip()
if statement in console.locals or statement not in REPL_COMMANDS:
@@ -125,12 +125,12 @@ def run_multiline_interactive_console(
command = REPL_COMMANDS[statement]
if callable(command):
# Make sure that history does not change because of commands
with reader.suspend_history():
with reader.suspend_history(), reader.suspend_colorization():
command()
return True
return False
while 1:
while True:
try:
try:
sys.stdout.flush()
@@ -148,20 +148,34 @@ def run_multiline_interactive_console(
continue
input_name = f"<python-input-{input_n}>"
linecache._register_code(input_name, statement, "<stdin>") # type: ignore[attr-defined]
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
assert not more
try:
append_history_file()
except (FileNotFoundError, PermissionError, OSError) as e:
warnings.warn(f"failed to open the history file for writing: {e}")
input_n += 1
except KeyboardInterrupt:
r = _get_reader()
r.cmpltn_reset()
if r.input_trans is r.isearch_trans:
r.do_cmd(("isearch-end", [""]))
r.pos = len(r.get_unicode())
r.dirty = True
r.refresh()
r.in_bracketed_paste = False
console.write("\nKeyboardInterrupt\n")
console.resetbuffer()
except MemoryError:
console.write("\nMemoryError\n")
console.resetbuffer()
except SystemExit:
raise
except:
console.showtraceback()
console.resetbuffer()
if show_ref_count:
console.write(
f"[{sys.gettotalrefcount()} refs,"
f" {sys.getallocatedblocks()} blocks]\n"
)
+488
View File
@@ -0,0 +1,488 @@
"""Pure Python curses-like terminal capability queries."""
from dataclasses import dataclass, field
import errno
import os
from pathlib import Path
import re
import struct
# Terminfo constants
MAGIC16 = 0o432 # Magic number for 16-bit terminfo format
MAGIC32 = 0o1036 # Magic number for 32-bit terminfo format
# Special values for absent/cancelled capabilities
ABSENT_BOOLEAN = -1
ABSENT_NUMERIC = -1
CANCELLED_NUMERIC = -2
ABSENT_STRING = None
CANCELLED_STRING = None
# Standard string capability names from ncurses Caps file
# This matches the order used by ncurses when compiling terminfo
# fmt: off
_STRING_NAMES: tuple[str, ...] = (
"cbt", "bel", "cr", "csr", "tbc", "clear", "el", "ed", "hpa", "cmdch",
"cup", "cud1", "home", "civis", "cub1", "mrcup", "cnorm", "cuf1", "ll",
"cuu1", "cvvis", "dch1", "dl1", "dsl", "hd", "smacs", "blink", "bold",
"smcup", "smdc", "dim", "smir", "invis", "prot", "rev", "smso", "smul",
"ech", "rmacs", "sgr0", "rmcup", "rmdc", "rmir", "rmso", "rmul", "flash",
"ff", "fsl", "is1", "is2", "is3", "if", "ich1", "il1", "ip", "kbs", "ktbc",
"kclr", "kctab", "kdch1", "kdl1", "kcud1", "krmir", "kel", "ked", "kf0",
"kf1", "kf10", "kf2", "kf3", "kf4", "kf5", "kf6", "kf7", "kf8", "kf9",
"khome", "kich1", "kil1", "kcub1", "kll", "knp", "kpp", "kcuf1", "kind",
"kri", "khts", "kcuu1", "rmkx", "smkx", "lf0", "lf1", "lf10", "lf2", "lf3",
"lf4", "lf5", "lf6", "lf7", "lf8", "lf9", "rmm", "smm", "nel", "pad", "dch",
"dl", "cud", "ich", "indn", "il", "cub", "cuf", "rin", "cuu", "pfkey",
"pfloc", "pfx", "mc0", "mc4", "mc5", "rep", "rs1", "rs2", "rs3", "rf", "rc",
"vpa", "sc", "ind", "ri", "sgr", "hts", "wind", "ht", "tsl", "uc", "hu",
"iprog", "ka1", "ka3", "kb2", "kc1", "kc3", "mc5p", "rmp", "acsc", "pln",
"kcbt", "smxon", "rmxon", "smam", "rmam", "xonc", "xoffc", "enacs", "smln",
"rmln", "kbeg", "kcan", "kclo", "kcmd", "kcpy", "kcrt", "kend", "kent",
"kext", "kfnd", "khlp", "kmrk", "kmsg", "kmov", "knxt", "kopn", "kopt",
"kprv", "kprt", "krdo", "kref", "krfr", "krpl", "krst", "kres", "ksav",
"kspd", "kund", "kBEG", "kCAN", "kCMD", "kCPY", "kCRT", "kDC", "kDL",
"kslt", "kEND", "kEOL", "kEXT", "kFND", "kHLP", "kHOM", "kIC", "kLFT",
"kMSG", "kMOV", "kNXT", "kOPT", "kPRV", "kPRT", "kRDO", "kRPL", "kRIT",
"kRES", "kSAV", "kSPD", "kUND", "rfi", "kf11", "kf12", "kf13", "kf14",
"kf15", "kf16", "kf17", "kf18", "kf19", "kf20", "kf21", "kf22", "kf23",
"kf24", "kf25", "kf26", "kf27", "kf28", "kf29", "kf30", "kf31", "kf32",
"kf33", "kf34", "kf35", "kf36", "kf37", "kf38", "kf39", "kf40", "kf41",
"kf42", "kf43", "kf44", "kf45", "kf46", "kf47", "kf48", "kf49", "kf50",
"kf51", "kf52", "kf53", "kf54", "kf55", "kf56", "kf57", "kf58", "kf59",
"kf60", "kf61", "kf62", "kf63", "el1", "mgc", "smgl", "smgr", "fln", "sclk",
"dclk", "rmclk", "cwin", "wingo", "hup","dial", "qdial", "tone", "pulse",
"hook", "pause", "wait", "u0", "u1", "u2", "u3", "u4", "u5", "u6", "u7",
"u8", "u9", "op", "oc", "initc", "initp", "scp", "setf", "setb", "cpi",
"lpi", "chr", "cvr", "defc", "swidm", "sdrfq", "sitm", "slm", "smicm",
"snlq", "snrmq", "sshm", "ssubm", "ssupm", "sum", "rwidm", "ritm", "rlm",
"rmicm", "rshm", "rsubm", "rsupm", "rum", "mhpa", "mcud1", "mcub1", "mcuf1",
"mvpa", "mcuu1", "porder", "mcud", "mcub", "mcuf", "mcuu", "scs", "smgb",
"smgbp", "smglp", "smgrp", "smgt", "smgtp", "sbim", "scsd", "rbim", "rcsd",
"subcs", "supcs", "docr", "zerom", "csnm", "kmous", "minfo", "reqmp",
"getm", "setaf", "setab", "pfxl", "devt", "csin", "s0ds", "s1ds", "s2ds",
"s3ds", "smglr", "smgtb", "birep", "binel", "bicr", "colornm", "defbi",
"endbi", "setcolor", "slines", "dispc", "smpch", "rmpch", "smsc", "rmsc",
"pctrm", "scesc", "scesa", "ehhlm", "elhlm", "elohlm", "erhlm", "ethlm",
"evhlm", "sgr1", "slength", "OTi2", "OTrs", "OTnl", "OTbc", "OTko", "OTma",
"OTG2", "OTG3", "OTG1", "OTG4", "OTGR", "OTGL", "OTGU", "OTGD", "OTGH",
"OTGV", "OTGC","meml", "memu", "box1"
)
# fmt: on
def _get_terminfo_dirs() -> list[Path]:
"""Get list of directories to search for terminfo files.
Based on ncurses behavior in:
- ncurses/tinfo/db_iterator.c:_nc_next_db()
- ncurses/tinfo/read_entry.c:_nc_read_entry()
"""
dirs = []
terminfo = os.environ.get("TERMINFO")
if terminfo:
dirs.append(terminfo)
try:
home = Path.home()
dirs.append(str(home / ".terminfo"))
except RuntimeError:
pass
# Check TERMINFO_DIRS
terminfo_dirs = os.environ.get("TERMINFO_DIRS", "")
if terminfo_dirs:
for d in terminfo_dirs.split(":"):
if d:
dirs.append(d)
dirs.extend(
[
"/etc/terminfo",
"/lib/terminfo",
"/usr/lib/terminfo",
"/usr/share/terminfo",
"/usr/share/lib/terminfo",
"/usr/share/misc/terminfo",
"/usr/local/lib/terminfo",
"/usr/local/share/terminfo",
]
)
return [Path(d) for d in dirs if Path(d).is_dir()]
def _validate_terminal_name_or_raise(terminal_name: str) -> None:
if not isinstance(terminal_name, str):
raise TypeError("`terminal_name` must be a string")
if not terminal_name:
raise ValueError("`terminal_name` cannot be empty")
if "\x00" in terminal_name:
raise ValueError("NUL character found in `terminal_name`")
t = Path(terminal_name)
if len(t.parts) > 1:
raise ValueError("`terminal_name` cannot contain path separators")
def _read_terminfo_file(terminal_name: str) -> bytes:
"""Find and read terminfo file for given terminal name.
Terminfo files are stored in directories using the first character
of the terminal name as a subdirectory.
"""
_validate_terminal_name_or_raise(terminal_name)
first_char = terminal_name[0].lower()
filename = terminal_name
for directory in _get_terminfo_dirs():
path = directory / first_char / filename
if path.is_file():
return path.read_bytes()
# Try with hex encoding of first char (for special chars)
hex_dir = "%02x" % ord(first_char)
path = directory / hex_dir / filename
if path.is_file():
return path.read_bytes()
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), filename)
# Hard-coded terminal capabilities for common terminals
# This is a minimal subset needed by PyREPL
_TERMINAL_CAPABILITIES = {
# ANSI/xterm-compatible terminals
"ansi": {
# Bell
"bel": b"\x07",
# Cursor movement
"cub": b"\x1b[%p1%dD", # Move cursor left N columns
"cud": b"\x1b[%p1%dB", # Move cursor down N rows
"cuf": b"\x1b[%p1%dC", # Move cursor right N columns
"cuu": b"\x1b[%p1%dA", # Move cursor up N rows
"cub1": b"\x08", # Move cursor left 1 column
"cud1": b"\n", # Move cursor down 1 row
"cuf1": b"\x1b[C", # Move cursor right 1 column
"cuu1": b"\x1b[A", # Move cursor up 1 row
"cup": b"\x1b[%i%p1%d;%p2%dH", # Move cursor to row, column
"hpa": b"\x1b[%i%p1%dG", # Move cursor to column
# Clear operations
"clear": b"\x1b[H\x1b[2J", # Clear screen and home cursor
"el": b"\x1b[K", # Clear to end of line
# Insert/delete
"dch": b"\x1b[%p1%dP", # Delete N characters
"dch1": b"\x1b[P", # Delete 1 character
"ich": b"\x1b[%p1%d@", # Insert N characters
"ich1": b"", # Insert 1 character
# Cursor visibility
"civis": b"\x1b[?25l", # Make cursor invisible
"cnorm": b"\x1b[?12l\x1b[?25h", # Make cursor normal (visible)
# Scrolling
"ind": b"\n", # Scroll up one line
"ri": b"\x1bM", # Scroll down one line
# Keypad mode
"smkx": b"\x1b[?1h\x1b=", # Enable keypad mode
"rmkx": b"\x1b[?1l\x1b>", # Disable keypad mode
# Padding (not used in modern terminals)
"pad": b"",
# Function keys and special keys
"kdch1": b"\x1b[3~", # Delete key
"kcud1": b"\x1bOB", # Down arrow
"kend": b"\x1bOF", # End key
"kent": b"\x1bOM", # Enter key
"khome": b"\x1bOH", # Home key
"kich1": b"\x1b[2~", # Insert key
"kcub1": b"\x1bOD", # Left arrow
"knp": b"\x1b[6~", # Page down
"kpp": b"\x1b[5~", # Page up
"kcuf1": b"\x1bOC", # Right arrow
"kcuu1": b"\x1bOA", # Up arrow
# Function keys F1-F20
"kf1": b"\x1bOP",
"kf2": b"\x1bOQ",
"kf3": b"\x1bOR",
"kf4": b"\x1bOS",
"kf5": b"\x1b[15~",
"kf6": b"\x1b[17~",
"kf7": b"\x1b[18~",
"kf8": b"\x1b[19~",
"kf9": b"\x1b[20~",
"kf10": b"\x1b[21~",
"kf11": b"\x1b[23~",
"kf12": b"\x1b[24~",
"kf13": b"\x1b[1;2P",
"kf14": b"\x1b[1;2Q",
"kf15": b"\x1b[1;2R",
"kf16": b"\x1b[1;2S",
"kf17": b"\x1b[15;2~",
"kf18": b"\x1b[17;2~",
"kf19": b"\x1b[18;2~",
"kf20": b"\x1b[19;2~",
},
# Dumb terminal - minimal capabilities
"dumb": {
"bel": b"\x07", # Bell
"cud1": b"\n", # Move down 1 row (newline)
"ind": b"\n", # Scroll up one line (newline)
},
# Linux console
"linux": {
# Bell
"bel": b"\x07",
# Cursor movement
"cub": b"\x1b[%p1%dD", # Move cursor left N columns
"cud": b"\x1b[%p1%dB", # Move cursor down N rows
"cuf": b"\x1b[%p1%dC", # Move cursor right N columns
"cuu": b"\x1b[%p1%dA", # Move cursor up N rows
"cub1": b"\x08", # Move cursor left 1 column (backspace)
"cud1": b"\n", # Move cursor down 1 row (newline)
"cuf1": b"\x1b[C", # Move cursor right 1 column
"cuu1": b"\x1b[A", # Move cursor up 1 row
"cup": b"\x1b[%i%p1%d;%p2%dH", # Move cursor to row, column
"hpa": b"\x1b[%i%p1%dG", # Move cursor to column
# Clear operations
"clear": b"\x1b[H\x1b[J", # Clear screen and home cursor (different from ansi!)
"el": b"\x1b[K", # Clear to end of line
# Insert/delete
"dch": b"\x1b[%p1%dP", # Delete N characters
"dch1": b"\x1b[P", # Delete 1 character
"ich": b"\x1b[%p1%d@", # Insert N characters
"ich1": b"\x1b[@", # Insert 1 character
# Cursor visibility
"civis": b"\x1b[?25l\x1b[?1c", # Make cursor invisible
"cnorm": b"\x1b[?25h\x1b[?0c", # Make cursor normal
# Scrolling
"ind": b"\n", # Scroll up one line
"ri": b"\x1bM", # Scroll down one line
# Keypad mode
"smkx": b"\x1b[?1h\x1b=", # Enable keypad mode
"rmkx": b"\x1b[?1l\x1b>", # Disable keypad mode
# Function keys and special keys
"kdch1": b"\x1b[3~", # Delete key
"kcud1": b"\x1b[B", # Down arrow
"kend": b"\x1b[4~", # End key (different from ansi!)
"khome": b"\x1b[1~", # Home key (different from ansi!)
"kich1": b"\x1b[2~", # Insert key
"kcub1": b"\x1b[D", # Left arrow
"knp": b"\x1b[6~", # Page down
"kpp": b"\x1b[5~", # Page up
"kcuf1": b"\x1b[C", # Right arrow
"kcuu1": b"\x1b[A", # Up arrow
# Function keys
"kf1": b"\x1b[[A",
"kf2": b"\x1b[[B",
"kf3": b"\x1b[[C",
"kf4": b"\x1b[[D",
"kf5": b"\x1b[[E",
"kf6": b"\x1b[17~",
"kf7": b"\x1b[18~",
"kf8": b"\x1b[19~",
"kf9": b"\x1b[20~",
"kf10": b"\x1b[21~",
"kf11": b"\x1b[23~",
"kf12": b"\x1b[24~",
"kf13": b"\x1b[25~",
"kf14": b"\x1b[26~",
"kf15": b"\x1b[28~",
"kf16": b"\x1b[29~",
"kf17": b"\x1b[31~",
"kf18": b"\x1b[32~",
"kf19": b"\x1b[33~",
"kf20": b"\x1b[34~",
},
}
# Map common TERM values to capability sets
_TERM_ALIASES = {
"xterm": "ansi",
"xterm-color": "ansi",
"xterm-256color": "ansi",
"screen": "ansi",
"screen-256color": "ansi",
"tmux": "ansi",
"tmux-256color": "ansi",
"vt100": "ansi",
"vt220": "ansi",
"rxvt": "ansi",
"rxvt-unicode": "ansi",
"rxvt-unicode-256color": "ansi",
"unknown": "dumb",
}
@dataclass
class TermInfo:
terminal_name: str | bytes | None
fallback: bool = True
_capabilities: dict[str, bytes] = field(default_factory=dict)
def __post_init__(self) -> None:
"""Initialize terminal capabilities for the given terminal type.
Based on ncurses implementation in:
- ncurses/tinfo/lib_setup.c:setupterm() and _nc_setupterm()
- ncurses/tinfo/lib_setup.c:TINFO_SETUP_TERM()
This version first attempts to read terminfo database files like ncurses,
then, if `fallback` is True, falls back to hardcoded capabilities for
common terminal types.
"""
# If termstr is None or empty, try to get from environment
if not self.terminal_name:
self.terminal_name = os.environ.get("TERM") or "ANSI"
if isinstance(self.terminal_name, bytes):
self.terminal_name = self.terminal_name.decode("ascii")
try:
self._parse_terminfo_file(self.terminal_name)
except (OSError, ValueError):
if not self.fallback:
raise
term_type = _TERM_ALIASES.get(
self.terminal_name, self.terminal_name
)
if term_type not in _TERMINAL_CAPABILITIES:
term_type = "dumb"
self._capabilities = _TERMINAL_CAPABILITIES[term_type].copy()
def _parse_terminfo_file(self, terminal_name: str) -> None:
"""Parse a terminfo file.
Populate the _capabilities dict for easy retrieval
Based on ncurses implementation in:
- ncurses/tinfo/read_entry.c:_nc_read_termtype()
- ncurses/tinfo/read_entry.c:_nc_read_file_entry()
- ncurses/tinfo/lib_ti.c:tigetstr()
"""
data = _read_terminfo_file(terminal_name)
too_short = f"TermInfo file for {terminal_name!r} too short"
offset = 12
if len(data) < offset:
raise ValueError(too_short)
magic, name_size, bool_count, num_count, str_count, str_size = (
struct.unpack("<Hhhhhh", data[:offset])
)
if magic == MAGIC16:
number_size = 2
elif magic == MAGIC32:
number_size = 4
else:
raise ValueError(
f"TermInfo file for {terminal_name!r} uses unknown magic"
)
# Skip data than PyREPL doesn't need:
# - names (`|`-separated ASCII strings)
# - boolean capabilities (bytes with value 0 or 1)
# - numbers (little-endian integers, `number_size` bytes each)
offset += name_size
offset += bool_count
if offset % 2:
# Align to even byte boundary for numbers
offset += 1
offset += num_count * number_size
if offset > len(data):
raise ValueError(too_short)
# Read string offsets
end_offset = offset + 2 * str_count
if offset > len(data):
raise ValueError(too_short)
string_offset_data = data[offset:end_offset]
string_offsets = [
off for [off] in struct.iter_unpack("<h", string_offset_data)
]
offset = end_offset
# Read string table
if offset + str_size > len(data):
raise ValueError(too_short)
string_table = data[offset : offset + str_size]
# Extract strings from string table
capabilities = {}
for cap, off in zip(_STRING_NAMES, string_offsets):
if off < 0:
# CANCELLED_STRING; we do not store those
continue
elif off < len(string_table):
# Find null terminator
end = string_table.find(0, off)
if end >= 0:
capabilities[cap] = string_table[off:end]
# in other cases this is ABSENT_STRING; we don't store those.
# Note: we don't support extended capabilities since PyREPL doesn't
# need them.
self._capabilities = capabilities
def get(self, cap: str) -> bytes | None:
"""Get terminal capability string by name.
"""
if not isinstance(cap, str):
raise TypeError(f"`cap` must be a string, not {type(cap)}")
return self._capabilities.get(cap)
def tparm(cap_bytes: bytes, *params: int) -> bytes:
"""Parameterize a terminal capability string.
Based on ncurses implementation in:
- ncurses/tinfo/lib_tparm.c:tparm()
- ncurses/tinfo/lib_tparm.c:tparam_internal()
The ncurses version implements a full stack-based interpreter for
terminfo parameter strings. This pure Python version implements only
the subset of parameter substitution operations needed by PyREPL:
- %i (increment parameters for 1-based indexing)
- %p[1-9]%d (parameter substitution)
- %p[1-9]%{n}%+%d (parameter plus constant)
"""
if not isinstance(cap_bytes, bytes):
raise TypeError(f"`cap` must be bytes, not {type(cap_bytes)}")
result = cap_bytes
# %i - increment parameters (1-based instead of 0-based)
increment = b"%i" in result
if increment:
result = result.replace(b"%i", b"")
# Replace %p1%d, %p2%d, etc. with actual parameter values
for i in range(len(params)):
pattern = b"%%p%d%%d" % (i + 1)
if pattern in result:
value = params[i]
if increment:
value += 1
result = result.replace(pattern, str(value).encode("ascii"))
# Handle %p1%{1}%+%d (parameter plus constant)
# Used in some cursor positioning sequences
pattern_re = re.compile(rb"%p(\d)%\{(\d+)\}%\+%d")
matches = list(pattern_re.finditer(result))
for match in reversed(matches): # reversed to maintain positions
param_idx = int(match.group(1))
constant = int(match.group(2))
value = params[param_idx] + constant
result = (
result[: match.start()]
+ str(value).encode("ascii")
+ result[match.end() :]
)
return result
+20 -7
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
import sys
# types
if False:
@@ -12,10 +13,22 @@ if trace_filename := os.environ.get("PYREPL_TRACE"):
trace_file = open(trace_filename, "a")
def trace(line: str, *k: object, **kw: object) -> None:
if trace_file is None:
return
if k or kw:
line = line.format(*k, **kw)
trace_file.write(line + "\n")
trace_file.flush()
if sys.platform == "emscripten":
from posix import _emscripten_log
def trace(line: str, *k: object, **kw: object) -> None:
if "PYREPL_TRACE" not in os.environ:
return
if k or kw:
line = line.format(*k, **kw)
_emscripten_log(line)
else:
def trace(line: str, *k: object, **kw: object) -> None:
if trace_file is None:
return
if k or kw:
line = line.format(*k, **kw)
trace_file.write(line + "\n")
trace_file.flush()
+8 -6
View File
@@ -1,8 +1,10 @@
from collections.abc import Callable, Iterator
Callback = Callable[[], object]
SimpleContextManager = Iterator[None]
KeySpec = str # like r"\C-c"
CommandName = str # like "interrupt"
EventTuple = tuple[CommandName, str]
Completer = Callable[[str, int], str | None]
type Callback = Callable[[], object]
type SimpleContextManager = Iterator[None]
type KeySpec = str # like r"\C-c"
type CommandName = str # like "interrupt"
type EventTuple = tuple[CommandName, str]
type Completer = Callable[[str, int], str | None]
type CharBuffer = list[str]
type CharWidths = list[int]
+82 -50
View File
@@ -29,31 +29,41 @@ import signal
import struct
import termios
import time
import types
import platform
from fcntl import ioctl
from . import curses
from . import terminfo
from .console import Console, Event
from .fancy_termios import tcgetattr, tcsetattr
from .fancy_termios import tcgetattr, tcsetattr, TermState
from .trace import trace
from .unix_eventqueue import EventQueue
from .utils import wlen
# declare posix optional to allow None assignment on other platforms
posix: types.ModuleType | None
try:
import posix
except ImportError:
posix = None
TYPE_CHECKING = False
# types
if TYPE_CHECKING:
from typing import IO, Literal, overload
from typing import AbstractSet, IO, Literal, overload, cast
else:
overload = lambda func: None
cast = lambda typ, val: val
class InvalidTerminal(RuntimeError):
pass
def __init__(self, message: str) -> None:
super().__init__(errno.EIO, message)
_error = (termios.error, curses.error, InvalidTerminal)
_error = (termios.error, InvalidTerminal)
_error_codes_to_ignore = frozenset([errno.EIO, errno.ENXIO, errno.EPERM])
SIGWINCH_EVENT = "repaint"
@@ -110,7 +120,7 @@ delayprog = re.compile(b"\\$<([0-9]+)((?:/|\\*){0,2})>")
try:
poll: type[select.poll] = select.poll
except AttributeError:
# this is exactly the minumum necessary to support what we
# this is exactly the minimum necessary to support what we
# do with poll objects
class MinimalPoll:
def __init__(self):
@@ -118,12 +128,13 @@ except AttributeError:
def register(self, fd, flag):
self.fd = fd
# note: The 'timeout' argument is received as *milliseconds*
def poll(self, timeout: float | None = None) -> list[int]:
if timeout is None:
r, w, e = select.select([self.fd], [], [])
else:
r, w, e = select.select([self.fd], [], [], timeout/1000)
r, w, e = select.select([self.fd], [], [], timeout / 1000)
return r
poll = MinimalPoll # type: ignore[assignment]
@@ -150,19 +161,28 @@ class UnixConsole(Console):
self.pollob = poll()
self.pollob.register(self.input_fd, select.POLLIN)
self.input_buffer = b""
self.input_buffer_pos = 0
curses.setupterm(term or None, self.output_fd)
self.terminfo = terminfo.TermInfo(term or None)
self.term = term
self.is_apple_terminal = (
platform.system() == "Darwin"
and os.getenv("TERM_PROGRAM") == "Apple_Terminal"
)
try:
self.__input_fd_set(tcgetattr(self.input_fd), ignore=frozenset())
except _error as e:
raise RuntimeError(f"termios failure ({e.args[1]})")
@overload
def _my_getstr(cap: str, optional: Literal[False] = False) -> bytes: ...
def _my_getstr(
cap: str, optional: Literal[False] = False
) -> bytes: ...
@overload
def _my_getstr(cap: str, optional: bool) -> bytes | None: ...
def _my_getstr(cap: str, optional: bool = False) -> bytes | None:
r = curses.tigetstr(cap)
r = self.terminfo.get(cap)
if not optional and r is None:
raise InvalidTerminal(
f"terminal doesn't have the required {cap} capability"
@@ -196,26 +216,19 @@ class UnixConsole(Console):
self.__setup_movement()
self.event_queue = EventQueue(self.input_fd, self.encoding)
self.event_queue = EventQueue(
self.input_fd, self.encoding, self.terminfo
)
self.cursor_visible = 1
def more_in_buffer(self) -> bool:
return bool(
self.input_buffer
and self.input_buffer_pos < len(self.input_buffer)
)
signal.signal(signal.SIGCONT, self._sigcont_handler)
def _sigcont_handler(self, signum, frame):
self.restore()
self.prepare()
def __read(self, n: int) -> bytes:
if not self.more_in_buffer():
self.input_buffer = os.read(self.input_fd, 10000)
ret = self.input_buffer[self.input_buffer_pos : self.input_buffer_pos + n]
self.input_buffer_pos += len(ret)
if self.input_buffer_pos >= len(self.input_buffer):
self.input_buffer = b""
self.input_buffer_pos = 0
return ret
return os.read(self.input_fd, n)
def change_encoding(self, encoding: str) -> None:
"""
@@ -238,8 +251,9 @@ class UnixConsole(Console):
if not self.__gone_tall:
while len(self.screen) < min(len(screen), self.height):
self.__hide_cursor()
self.__move(0, len(self.screen) - 1)
self.__write("\n")
if self.screen:
self.__move(0, len(self.screen) - 1)
self.__write("\n")
self.posxy = 0, len(self.screen)
self.screen.append("")
else:
@@ -328,6 +342,8 @@ class UnixConsole(Console):
"""
Prepare the console for input/output operations.
"""
self.__buffer = []
self.__svtermstate = tcgetattr(self.input_fd)
raw = self.__svtermstate.copy()
raw.iflag &= ~(termios.INPCK | termios.ISTRIP | termios.IXON)
@@ -339,17 +355,15 @@ class UnixConsole(Console):
raw.lflag |= termios.ISIG
raw.cc[termios.VMIN] = 1
raw.cc[termios.VTIME] = 0
tcsetattr(self.input_fd, termios.TCSADRAIN, raw)
self.__input_fd_set(raw)
# In macOS terminal we need to deactivate line wrap via ANSI escape code
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
if self.is_apple_terminal:
os.write(self.output_fd, b"\033[?7l")
self.screen = []
self.height, self.width = self.getheightwidth()
self.__buffer = []
self.posxy = 0, 0
self.__gone_tall = 0
self.__move = self.__move_short
@@ -371,13 +385,18 @@ class UnixConsole(Console):
self.__disable_bracketed_paste()
self.__maybe_write_code(self._rmkx)
self.flushoutput()
tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate)
self.__input_fd_set(self.__svtermstate)
if platform.system() == "Darwin" and os.getenv("TERM_PROGRAM") == "Apple_Terminal":
if self.is_apple_terminal:
os.write(self.output_fd, b"\033[?7h")
if hasattr(self, "old_sigwinch"):
signal.signal(signal.SIGWINCH, self.old_sigwinch)
try:
signal.signal(signal.SIGWINCH, self.old_sigwinch)
except ValueError as e:
import threading
if threading.current_thread() is threading.main_thread():
raise e
del self.old_sigwinch
def push_char(self, char: int | bytes) -> None:
@@ -410,6 +429,8 @@ class UnixConsole(Console):
return self.event_queue.get()
else:
continue
elif err.errno == errno.EIO:
raise SystemExit(errno.EIO)
else:
raise
else:
@@ -422,7 +443,6 @@ class UnixConsole(Console):
"""
return (
not self.event_queue.empty()
or self.more_in_buffer()
or bool(self.pollob.poll(timeout))
)
@@ -525,6 +545,7 @@ class UnixConsole(Console):
e.raw += e.raw
amount = struct.unpack("i", ioctl(self.input_fd, FIONREAD, b"\0\0\0\0"))[0]
trace("getpending({a})", a=amount)
raw = self.__read(amount)
data = str(raw, self.encoding, "replace")
e.data += data
@@ -566,11 +587,9 @@ class UnixConsole(Console):
@property
def input_hook(self):
try:
import posix
except ImportError:
return None
if posix._is_inputhook_installed():
# avoid inline imports here so the repl doesn't get flooded
# with import logging from -X importtime=2
if posix is not None and posix._is_inputhook_installed():
return posix._inputhook
def __enable_bracketed_paste(self) -> None:
@@ -602,14 +621,14 @@ class UnixConsole(Console):
if self._dch1:
self.dch1 = self._dch1
elif self._dch:
self.dch1 = curses.tparm(self._dch, 1)
self.dch1 = terminfo.tparm(self._dch, 1)
else:
self.dch1 = None
if self._ich1:
self.ich1 = self._ich1
elif self._ich:
self.ich1 = curses.tparm(self._ich, 1)
self.ich1 = terminfo.tparm(self._ich, 1)
else:
self.ich1 = None
@@ -634,7 +653,7 @@ class UnixConsole(Console):
# reuse the oldline as much as possible, but stop as soon as we
# encounter an ESCAPE, because it might be the start of an escape
# sequene
# sequence
while (
x_coord < minlen
and oldline[x_pos] == newline[x_pos]
@@ -706,7 +725,7 @@ class UnixConsole(Console):
self.__buffer.append((text, 0))
def __write_code(self, fmt, *args):
self.__buffer.append((curses.tparm(fmt, *args), 1))
self.__buffer.append((terminfo.tparm(fmt, *args), 1))
def __maybe_write_code(self, fmt, *args):
if fmt:
@@ -757,7 +776,6 @@ class UnixConsole(Console):
self.__write_code(self._cup, y - self.__offset, x)
def __sigwinch(self, signum, frame):
self.height, self.width = self.getheightwidth()
self.event_queue.insert(Event("resize", None))
def __hide_cursor(self):
@@ -790,9 +808,9 @@ class UnixConsole(Console):
will never do anyone any good."""
# using .get() means that things will blow up
# only if the bps is actually needed (which I'm
# betting is pretty unlkely)
# betting is pretty unlikely)
bps = ratedict.get(self.__svtermstate.ospeed)
while 1:
while True:
m = prog.search(fmt)
if not m:
os.write(self.output_fd, fmt)
@@ -808,3 +826,17 @@ class UnixConsole(Console):
os.write(self.output_fd, self._pad * nchars)
else:
time.sleep(float(delay) / 1000.0)
def __input_fd_set(
self,
state: TermState,
ignore: AbstractSet[int] = _error_codes_to_ignore,
) -> bool:
try:
tcsetattr(self.input_fd, termios.TCSADRAIN, state)
except termios.error as te:
if te.args[0] not in ignore:
raise
return False
else:
return True
+10 -85
View File
@@ -18,12 +18,9 @@
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
from collections import deque
from . import keymap
from .console import Event
from . import curses
from .terminfo import TermInfo
from .trace import trace
from .base_eventqueue import BaseEventQueue
from termios import tcgetattr, VERASE
import os
@@ -57,96 +54,24 @@ CTRL_ARROW_KEYCODES= {
b'\033Oc': 'ctrl right',
}
def get_terminal_keycodes() -> dict[bytes, str]:
def get_terminal_keycodes(ti: TermInfo) -> dict[bytes, str]:
"""
Generates a dictionary mapping terminal keycodes to human-readable names.
"""
keycodes = {}
for key, terminal_code in TERMINAL_KEYNAMES.items():
keycode = curses.tigetstr(terminal_code)
keycode = ti.get(terminal_code)
trace('key {key} tiname {terminal_code} keycode {keycode!r}', **locals())
if keycode:
keycodes[keycode] = key
keycodes.update(CTRL_ARROW_KEYCODES)
return keycodes
class EventQueue:
def __init__(self, fd: int, encoding: str) -> None:
self.keycodes = get_terminal_keycodes()
class EventQueue(BaseEventQueue):
def __init__(self, fd: int, encoding: str, ti: TermInfo) -> None:
keycodes = get_terminal_keycodes(ti)
if os.isatty(fd):
backspace = tcgetattr(fd)[6][VERASE]
self.keycodes[backspace] = "backspace"
self.compiled_keymap = keymap.compile_keymap(self.keycodes)
self.keymap = self.compiled_keymap
trace("keymap {k!r}", k=self.keymap)
self.encoding = encoding
self.events: deque[Event] = deque()
self.buf = bytearray()
def get(self) -> Event | None:
"""
Retrieves the next event from the queue.
"""
if self.events:
return self.events.popleft()
else:
return None
def empty(self) -> bool:
"""
Checks if the queue is empty.
"""
return not self.events
def flush_buf(self) -> bytearray:
"""
Flushes the buffer and returns its contents.
"""
old = self.buf
self.buf = bytearray()
return old
def insert(self, event: Event) -> None:
"""
Inserts an event into the queue.
"""
trace('added event {event}', event=event)
self.events.append(event)
def push(self, char: int | bytes) -> None:
"""
Processes a character by updating the buffer and handling special key mappings.
"""
ord_char = char if isinstance(char, int) else ord(char)
char = bytes(bytearray((ord_char,)))
self.buf.append(ord_char)
if char in self.keymap:
if self.keymap is self.compiled_keymap:
#sanity check, buffer is empty when a special key comes
assert len(self.buf) == 1
k = self.keymap[char]
trace('found map {k!r}', k=k)
if isinstance(k, dict):
self.keymap = k
else:
self.insert(Event('key', k, self.flush_buf()))
self.keymap = self.compiled_keymap
elif self.buf and self.buf[0] == 27: # escape
# escape sequence not recognized by our keymap: propagate it
# outside so that i can be recognized as an M-... key (see also
# the docstring in keymap.py
trace('unrecognized escape sequence, propagating...')
self.keymap = self.compiled_keymap
self.insert(Event('key', '\033', bytearray(b'\033')))
for _c in self.flush_buf()[1:]:
self.push(_c)
else:
try:
decoded = bytes(self.buf).decode(self.encoding)
except UnicodeError:
return
else:
self.insert(Event('key', decoded, self.flush_buf()))
self.keymap = self.compiled_keymap
keycodes[backspace] = "backspace"
BaseEventQueue.__init__(self, encoding, keycodes)
+371 -5
View File
@@ -1,25 +1,391 @@
import re
import unicodedata
from __future__ import annotations
import builtins
import functools
import keyword
import re
import token as T
import tokenize
import unicodedata
import _colorize
from collections import deque
from io import StringIO
from tokenize import TokenInfo as TI
from typing import Iterable, Iterator, Match, NamedTuple, Self
from .types import CharBuffer, CharWidths
from .trace import trace
ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]")
ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02")
ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""})
IDENTIFIERS_AFTER = {"def", "class"}
KEYWORD_CONSTANTS = {"True", "False", "None"}
BUILTINS = {str(name) for name in dir(builtins) if not name.startswith('_')}
def THEME(**kwargs):
# Not cached: the user can modify the theme inside the interactive session.
return _colorize.get_theme(**kwargs).syntax
class Span(NamedTuple):
"""Span indexing that's inclusive on both ends."""
start: int
end: int
@classmethod
def from_re(cls, m: Match[str], group: int | str) -> Self:
re_span = m.span(group)
return cls(re_span[0], re_span[1] - 1)
@classmethod
def from_token(cls, token: TI, line_len: list[int]) -> Self:
end_offset = -1
if (token.type in {T.FSTRING_MIDDLE, T.TSTRING_MIDDLE}
and token.string.endswith(("{", "}"))):
# gh-134158: a visible trailing brace comes from a double brace in input
end_offset += 1
return cls(
line_len[token.start[0] - 1] + token.start[1],
line_len[token.end[0] - 1] + token.end[1] + end_offset,
)
class ColorSpan(NamedTuple):
span: Span
tag: str
@functools.cache
def str_width(c: str) -> int:
if ord(c) < 128:
return 1
# gh-139246 for zero-width joiner and combining characters
if unicodedata.combining(c):
return 0
category = unicodedata.category(c)
if category == "Cf" and c != "\u00ad":
return 0
w = unicodedata.east_asian_width(c)
if w in ('N', 'Na', 'H', 'A'):
if w in ("N", "Na", "H", "A"):
return 1
return 2
def wlen(s: str) -> int:
if len(s) == 1 and s != '\x1a':
if len(s) == 1 and s != "\x1a":
return str_width(s)
length = sum(str_width(i) for i in s)
# remove lengths of any escape sequences
sequence = ANSI_ESCAPE_SEQUENCE.findall(s)
ctrl_z_cnt = s.count('\x1a')
ctrl_z_cnt = s.count("\x1a")
return length - sum(len(i) for i in sequence) + ctrl_z_cnt
def unbracket(s: str, including_content: bool = False) -> str:
r"""Return `s` with \001 and \002 characters removed.
If `including_content` is True, content between \001 and \002 is also
stripped.
"""
if including_content:
return ZERO_WIDTH_BRACKET.sub("", s)
return s.translate(ZERO_WIDTH_TRANS)
def gen_colors(buffer: str) -> Iterator[ColorSpan]:
"""Returns a list of index spans to color using the given color tag.
The input `buffer` should be a valid start of a Python code block, i.e.
it cannot be a block starting in the middle of a multiline string.
"""
sio = StringIO(buffer)
line_lengths = [0] + [len(line) for line in sio.readlines()]
# make line_lengths cumulative
for i in range(1, len(line_lengths)):
line_lengths[i] += line_lengths[i-1]
sio.seek(0)
gen = tokenize.generate_tokens(sio.readline)
last_emitted: ColorSpan | None = None
try:
for color in gen_colors_from_token_stream(gen, line_lengths):
yield color
last_emitted = color
except SyntaxError:
return
except tokenize.TokenError as te:
yield from recover_unterminated_string(
te, line_lengths, last_emitted, buffer
)
def recover_unterminated_string(
exc: tokenize.TokenError,
line_lengths: list[int],
last_emitted: ColorSpan | None,
buffer: str,
) -> Iterator[ColorSpan]:
msg, loc = exc.args
if loc is None:
return
line_no, column = loc
if msg.startswith(
(
"unterminated string literal",
"unterminated f-string literal",
"unterminated t-string literal",
"EOF in multi-line string",
"unterminated triple-quoted f-string literal",
"unterminated triple-quoted t-string literal",
)
):
start = line_lengths[line_no - 1] + column - 1
end = line_lengths[-1] - 1
# in case FSTRING_START was already emitted
if last_emitted and start <= last_emitted.span.start:
trace("before last emitted = {s}", s=start)
start = last_emitted.span.end + 1
span = Span(start, end)
trace("yielding span {a} -> {b}", a=span.start, b=span.end)
yield ColorSpan(span, "string")
else:
trace(
"unhandled token error({buffer}) = {te}",
buffer=repr(buffer),
te=str(exc),
)
def gen_colors_from_token_stream(
token_generator: Iterator[TI],
line_lengths: list[int],
) -> Iterator[ColorSpan]:
token_window = prev_next_window(token_generator)
is_def_name = False
bracket_level = 0
for prev_token, token, next_token in token_window:
assert token is not None
if token.start == token.end:
continue
match token.type:
case (
T.STRING
| T.FSTRING_START | T.FSTRING_MIDDLE | T.FSTRING_END
| T.TSTRING_START | T.TSTRING_MIDDLE | T.TSTRING_END
):
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "string")
case T.COMMENT:
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "comment")
case T.NUMBER:
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "number")
case T.OP:
if token.string in "([{":
bracket_level += 1
elif token.string in ")]}":
bracket_level -= 1
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "op")
case T.NAME:
if is_def_name:
is_def_name = False
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "definition")
elif keyword.iskeyword(token.string):
span_cls = "keyword"
if token.string in KEYWORD_CONSTANTS:
span_cls = "keyword_constant"
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, span_cls)
if token.string in IDENTIFIERS_AFTER:
is_def_name = True
elif (
keyword.issoftkeyword(token.string)
and bracket_level == 0
and is_soft_keyword_used(prev_token, token, next_token)
):
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "soft_keyword")
elif (
token.string in BUILTINS
and not (prev_token and prev_token.exact_type == T.DOT)
):
span = Span.from_token(token, line_lengths)
yield ColorSpan(span, "builtin")
keyword_first_sets_match = {"False", "None", "True", "await", "lambda", "not"}
keyword_first_sets_case = {"False", "None", "True"}
def is_soft_keyword_used(*tokens: TI | None) -> bool:
"""Returns True if the current token is a keyword in this context.
For the `*tokens` to match anything, they have to be a three-tuple of
(previous, current, next).
"""
trace("is_soft_keyword_used{t}", t=tokens)
match tokens:
case (
None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"),
TI(string="match"),
TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START)
| TI(T.OP, string="(" | "*" | "[" | "{" | "~" | "...")
):
return True
case (
None | TI(T.NEWLINE) | TI(T.INDENT) | TI(string=":"),
TI(string="match"),
TI(T.NAME, string=s)
):
if keyword.iskeyword(s):
return s in keyword_first_sets_match
return True
case (
None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"),
TI(string="case"),
TI(T.NUMBER | T.STRING | T.FSTRING_START | T.TSTRING_START)
| TI(T.OP, string="(" | "*" | "-" | "[" | "{")
):
return True
case (
None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"),
TI(string="case"),
TI(T.NAME, string=s)
):
if keyword.iskeyword(s):
return s in keyword_first_sets_case
return True
case (TI(string="case"), TI(string="_"), TI(string=":")):
return True
case (
None | TI(T.NEWLINE) | TI(T.INDENT) | TI(T.DEDENT) | TI(string=":"),
TI(string="type"),
TI(T.NAME, string=s)
):
return not keyword.iskeyword(s)
case _:
return False
def disp_str(
buffer: str,
colors: list[ColorSpan] | None = None,
start_index: int = 0,
force_color: bool = False,
) -> tuple[CharBuffer, CharWidths]:
r"""Decompose the input buffer into a printable variant with applied colors.
Returns a tuple of two lists:
- the first list is the input buffer, character by character, with color
escape codes added (while those codes contain multiple ASCII characters,
each code is considered atomic *and is attached for the corresponding
visible character*);
- the second list is the visible width of each character in the input
buffer.
Note on colors:
- The `colors` list, if provided, is partially consumed within. We're using
a list and not a generator since we need to hold onto the current
unfinished span between calls to disp_str in case of multiline strings.
- The `colors` list is computed from the start of the input block. `buffer`
is only a subset of that input block, a single line within. This is why
we need `start_index` to inform us which position is the start of `buffer`
actually within user input. This allows us to match color spans correctly.
Examples:
>>> utils.disp_str("a = 9")
(['a', ' ', '=', ' ', '9'], [1, 1, 1, 1, 1])
>>> line = "while 1:"
>>> colors = list(utils.gen_colors(line))
>>> utils.disp_str(line, colors=colors)
(['\x1b[1;34mw', 'h', 'i', 'l', 'e\x1b[0m', ' ', '1', ':'], [1, 1, 1, 1, 1, 1, 1, 1])
"""
chars: CharBuffer = []
char_widths: CharWidths = []
if not buffer:
return chars, char_widths
while colors and colors[0].span.end < start_index:
# move past irrelevant spans
colors.pop(0)
theme = THEME(force_color=force_color)
pre_color = ""
post_color = ""
if colors and colors[0].span.start < start_index:
# looks like we're continuing a previous color (e.g. a multiline str)
pre_color = theme[colors[0].tag]
for i, c in enumerate(buffer, start_index):
if colors and colors[0].span.start == i: # new color starts now
pre_color = theme[colors[0].tag]
if c == "\x1a": # CTRL-Z on Windows
chars.append(c)
char_widths.append(2)
elif ord(c) < 128:
chars.append(c)
char_widths.append(1)
elif unicodedata.category(c).startswith("C"):
c = r"\u%04x" % ord(c)
chars.append(c)
char_widths.append(len(c))
else:
chars.append(c)
char_widths.append(str_width(c))
if colors and colors[0].span.end == i: # current color ends now
post_color = theme.reset
colors.pop(0)
chars[-1] = pre_color + chars[-1] + post_color
pre_color = ""
post_color = ""
if colors and colors[0].span.start < i and colors[0].span.end > i:
# even though the current color should be continued, reset it for now.
# the next call to `disp_str()` will revive it.
chars[-1] += theme.reset
return chars, char_widths
def prev_next_window[T](
iterable: Iterable[T]
) -> Iterator[tuple[T | None, ...]]:
"""Generates three-tuples of (previous, current, next) items.
On the first iteration previous is None. On the last iteration next
is None. In case of exception next is None and the exception is re-raised
on a subsequent next() call.
Inspired by `sliding_window` from `itertools` recipes.
"""
iterator = iter(iterable)
window = deque((None, next(iterator)), maxlen=3)
try:
for x in iterator:
window.append(x)
yield tuple(window)
except Exception:
raise
finally:
window.append(None)
yield tuple(window)
+183 -71
View File
@@ -22,11 +22,9 @@ from __future__ import annotations
import io
import os
import sys
import time
import msvcrt
from collections import deque
import ctypes
import types
from ctypes.wintypes import (
_COORD,
WORD,
@@ -42,9 +40,10 @@ from ctypes import Structure, POINTER, Union
from .console import Event, Console
from .trace import trace
from .utils import wlen
from .windows_eventqueue import EventQueue
try:
from ctypes import GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined]
from ctypes import get_last_error, GetLastError, WinDLL, windll, WinError # type: ignore[attr-defined]
except:
# Keep MyPy happy off Windows
from ctypes import CDLL as WinDLL, cdll as windll
@@ -52,11 +51,20 @@ except:
def GetLastError() -> int:
return 42
def get_last_error() -> int:
return 42
class WinError(OSError): # type: ignore[no-redef]
def __init__(self, err: int | None, descr: str | None = None) -> None:
self.err = err
self.descr = descr
# declare nt optional to allow None assignment on other platforms
nt: types.ModuleType | None
try:
import nt
except ImportError:
nt = None
TYPE_CHECKING = False
@@ -94,7 +102,9 @@ VK_MAP: dict[int, str] = {
0x83: "f20", # VK_F20
}
# Console escape codes: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
# Virtual terminal output sequences
# Reference: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences
# Check `windows_eventqueue.py` for input sequences
ERASE_IN_LINE = "\x1b[K"
MOVE_LEFT = "\x1b[{}D"
MOVE_RIGHT = "\x1b[{}C"
@@ -102,10 +112,25 @@ MOVE_UP = "\x1b[{}A"
MOVE_DOWN = "\x1b[{}B"
CLEAR = "\x1b[H\x1b[J"
# State of control keys: https://learn.microsoft.com/en-us/windows/console/key-event-record-str
ALT_ACTIVE = 0x01 | 0x02
CTRL_ACTIVE = 0x04 | 0x08
WAIT_TIMEOUT = 0x102
WAIT_FAILED = 0xFFFFFFFF
# from winbase.h
INFINITE = 0xFFFFFFFF
class _error(Exception):
pass
def _supports_vt():
try:
return nt._supports_virtual_terminal()
except AttributeError:
return False
class WindowsConsole(Console):
def __init__(
@@ -117,17 +142,29 @@ class WindowsConsole(Console):
):
super().__init__(f_in, f_out, term, encoding)
self.__vt_support = _supports_vt()
if self.__vt_support:
trace('console supports virtual terminal')
# Save original console modes so we can recover on cleanup.
original_input_mode = DWORD()
GetConsoleMode(InHandle, original_input_mode)
trace(f'saved original input mode 0x{original_input_mode.value:x}')
self.__original_input_mode = original_input_mode.value
SetConsoleMode(
OutHandle,
ENABLE_WRAP_AT_EOL_OUTPUT
| ENABLE_PROCESSED_OUTPUT
| ENABLE_VIRTUAL_TERMINAL_PROCESSING,
)
self.screen: list[str] = []
self.width = 80
self.height = 25
self.__offset = 0
self.event_queue: deque[Event] = deque()
self.event_queue = EventQueue(encoding)
try:
self.out = io._WindowsConsoleIO(self.output_fd, "w") # type: ignore[attr-defined]
except ValueError:
@@ -146,8 +183,9 @@ class WindowsConsole(Console):
while len(self.screen) < min(len(screen), self.height):
self._hide_cursor()
self._move_relative(0, len(self.screen) - 1)
self.__write("\n")
if self.screen:
self._move_relative(0, len(self.screen) - 1)
self.__write("\n")
self.posxy = 0, len(self.screen)
self.screen.append("")
@@ -204,11 +242,9 @@ class WindowsConsole(Console):
@property
def input_hook(self):
try:
import nt
except ImportError:
return None
if nt._is_inputhook_installed():
# avoid inline imports here so the repl doesn't get flooded
# with import logging from -X importtime=2
if nt is not None and nt._is_inputhook_installed():
return nt._inputhook
def __write_changed_line(
@@ -232,7 +268,7 @@ class WindowsConsole(Console):
# reuse the oldline as much as possible, but stop as soon as we
# encounter an ESCAPE, because it might be the start of an escape
# sequene
# sequence
while (
x_coord < minlen
and oldline[x_pos] == newline[x_pos]
@@ -247,18 +283,13 @@ class WindowsConsole(Console):
self._erase_to_end()
self.__write(newline[x_pos:])
if wlen(newline) == self.width:
# If we wrapped we want to start at the next line
self._move_relative(0, y + 1)
self.posxy = 0, y + 1
else:
self.posxy = wlen(newline), y
self.posxy = min(wlen(newline), self.width - 1), y
if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline:
# ANSI escape characters are present, so we can't assume
# anything about the position of the cursor. Moving the cursor
# to the left margin should work to get to a known position.
self.move_cursor(0, y)
if "\x1b" in newline or y != self.posxy[1] or '\x1a' in newline:
# ANSI escape characters are present, so we can't assume
# anything about the position of the cursor. Moving the cursor
# to the left margin should work to get to a known position.
self.move_cursor(0, y)
def _scroll(
self, top: int, bottom: int, left: int | None = None, right: int | None = None
@@ -291,6 +322,12 @@ class WindowsConsole(Console):
def _disable_blinking(self):
self.__write("\x1b[?12l")
def _enable_bracketed_paste(self) -> None:
self.__write("\x1b[?2004h")
def _disable_bracketed_paste(self) -> None:
self.__write("\x1b[?2004l")
def __write(self, text: str) -> None:
if "\x1a" in text:
text = ''.join(["^Z" if x == '\x1a' else x for x in text])
@@ -320,8 +357,15 @@ class WindowsConsole(Console):
self.__gone_tall = 0
self.__offset = 0
if self.__vt_support:
SetConsoleMode(InHandle, self.__original_input_mode | ENABLE_VIRTUAL_TERMINAL_INPUT)
self._enable_bracketed_paste()
def restore(self) -> None:
pass
if self.__vt_support:
# Recover to original mode before running REPL
self._disable_bracketed_paste()
SetConsoleMode(InHandle, self.__original_input_mode)
def _move_relative(self, x: int, y: int) -> None:
"""Moves relative to the current posxy"""
@@ -342,7 +386,7 @@ class WindowsConsole(Console):
raise ValueError(f"Bad cursor position {x}, {y}")
if y < self.__offset or y >= self.__offset + self.height:
self.event_queue.insert(0, Event("scroll", ""))
self.event_queue.insert(Event("scroll", ""))
else:
self._move_relative(x, y)
self.posxy = x, y
@@ -371,14 +415,7 @@ class WindowsConsole(Console):
return info.srWindow.Bottom # type: ignore[no-any-return]
def _read_input(self, block: bool = True) -> INPUT_RECORD | None:
if not block:
events = DWORD()
if not GetNumberOfConsoleInputEvents(InHandle, events):
raise WinError(GetLastError())
if not events.value:
return None
def _read_input(self) -> INPUT_RECORD | None:
rec = INPUT_RECORD()
read = DWORD()
if not ReadConsoleInput(InHandle, rec, 1, read):
@@ -386,15 +423,26 @@ class WindowsConsole(Console):
return rec
def _read_input_bulk(
self, n: int
) -> tuple[ctypes.Array[INPUT_RECORD], int]:
rec = (n * INPUT_RECORD)()
read = DWORD()
if not ReadConsoleInput(InHandle, rec, n, read):
raise WinError(GetLastError())
return rec, read.value
def get_event(self, block: bool = True) -> Event | None:
"""Return an Event instance. Returns None if |block| is false
and there is no event pending, otherwise waits for the
completion of an event."""
if self.event_queue:
return self.event_queue.pop()
while True:
rec = self._read_input(block)
if not block and not self.wait(timeout=0):
return None
while self.event_queue.empty():
rec = self._read_input()
if rec is None:
return None
@@ -407,31 +455,47 @@ class WindowsConsole(Console):
continue
return None
key = rec.Event.KeyEvent.uChar.UnicodeChar
key_event = rec.Event.KeyEvent
raw_key = key = key_event.uChar.UnicodeChar
if rec.Event.KeyEvent.uChar.UnicodeChar == "\r":
# Make enter make unix-like
return Event(evt="key", data="\n", raw=b"\n")
elif rec.Event.KeyEvent.wVirtualKeyCode == 8:
if key == "\r":
# Make enter unix-like
return Event(evt="key", data="\n")
elif key_event.wVirtualKeyCode == 8:
# Turn backspace directly into the command
return Event(
evt="key",
data="backspace",
raw=rec.Event.KeyEvent.uChar.UnicodeChar,
)
elif rec.Event.KeyEvent.uChar.UnicodeChar == "\x00":
key = "backspace"
elif key == "\x00":
# Handle special keys like arrow keys and translate them into the appropriate command
code = VK_MAP.get(rec.Event.KeyEvent.wVirtualKeyCode)
if code:
return Event(
evt="key", data=code, raw=rec.Event.KeyEvent.uChar.UnicodeChar
)
key = VK_MAP.get(key_event.wVirtualKeyCode)
if key:
if key_event.dwControlKeyState & CTRL_ACTIVE:
key = f"ctrl {key}"
elif key_event.dwControlKeyState & ALT_ACTIVE:
# queue the key, return the meta command
self.event_queue.insert(Event(evt="key", data=key))
return Event(evt="key", data="\033") # keymap.py uses this for meta
return Event(evt="key", data=key)
if block:
continue
return None
elif self.__vt_support:
# If virtual terminal is enabled, scanning VT sequences
for char in raw_key.encode(self.event_queue.encoding, "replace"):
self.event_queue.push(char)
continue
return Event(evt="key", data=key, raw=rec.Event.KeyEvent.uChar.UnicodeChar)
if key_event.dwControlKeyState & ALT_ACTIVE:
# Do not swallow characters that have been entered via AltGr:
# Windows internally converts AltGr to CTRL+ALT, see
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-vkkeyscanw
if not key_event.dwControlKeyState & CTRL_ACTIVE:
# queue the key, return the meta command
self.event_queue.insert(Event(evt="key", data=key))
return Event(evt="key", data="\033") # keymap.py uses this for meta
return Event(evt="key", data=key)
return self.event_queue.get()
def push_char(self, char: int | bytes) -> None:
"""
@@ -446,7 +510,7 @@ class WindowsConsole(Console):
"""Wipe the screen"""
self.__write(CLEAR)
self.posxy = 0, 0
self.screen = [""]
self.screen = []
def finish(self) -> None:
"""Move the cursor to the end of the display and otherwise get
@@ -472,18 +536,53 @@ class WindowsConsole(Console):
def getpending(self) -> Event:
"""Return the characters that have been typed but not yet
processed."""
return Event("key", "", b"")
e = Event("key", "", b"")
while not self.event_queue.empty():
e2 = self.event_queue.get()
if e2:
e.data += e2.data
recs, rec_count = self._read_input_bulk(1024)
for i in range(rec_count):
rec = recs[i]
# In case of a legacy console, we do not only receive a keydown
# event, but also a keyup event - and for uppercase letters
# an additional SHIFT_PRESSED event.
if rec and rec.EventType == KEY_EVENT:
key_event = rec.Event.KeyEvent
if not key_event.bKeyDown:
continue
ch = key_event.uChar.UnicodeChar
if ch == "\x00":
# ignore SHIFT_PRESSED and special keys
continue
if ch == "\r":
ch += "\n"
e.data += ch
return e
def wait_for_event(self, timeout: float | None) -> bool:
"""Wait for an event."""
if timeout is None:
timeout = INFINITE
else:
timeout = int(timeout)
ret = WaitForSingleObject(InHandle, timeout)
if ret == WAIT_FAILED:
raise WinError(get_last_error())
elif ret == WAIT_TIMEOUT:
return False
return True
def wait(self, timeout: float | None) -> bool:
"""Wait for an event."""
# Poor man's Windows select loop
start_time = time.time()
while True:
if msvcrt.kbhit(): # type: ignore[attr-defined]
return True
if timeout and time.time() - start_time > timeout / 1000:
return False
time.sleep(0.01)
"""
Wait for events on the console.
"""
return (
not self.event_queue.empty()
or self.wait_for_event(timeout)
)
def repaint(self) -> None:
raise NotImplementedError("No repaint support")
@@ -553,6 +652,13 @@ MENU_EVENT = 0x08
MOUSE_EVENT = 0x02
WINDOW_BUFFER_SIZE_EVENT = 0x04
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
ENABLE_PROCESSED_OUTPUT = 0x01
ENABLE_WRAP_AT_EOL_OUTPUT = 0x02
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x04
@@ -584,6 +690,10 @@ if sys.platform == "win32":
]
ScrollConsoleScreenBuffer.restype = BOOL
GetConsoleMode = _KERNEL32.GetConsoleMode
GetConsoleMode.argtypes = [HANDLE, POINTER(DWORD)]
GetConsoleMode.restype = BOOL
SetConsoleMode = _KERNEL32.SetConsoleMode
SetConsoleMode.argtypes = [HANDLE, DWORD]
SetConsoleMode.restype = BOOL
@@ -592,14 +702,15 @@ if sys.platform == "win32":
ReadConsoleInput.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, POINTER(DWORD)]
ReadConsoleInput.restype = BOOL
GetNumberOfConsoleInputEvents = _KERNEL32.GetNumberOfConsoleInputEvents
GetNumberOfConsoleInputEvents.argtypes = [HANDLE, POINTER(DWORD)]
GetNumberOfConsoleInputEvents.restype = BOOL
FlushConsoleInputBuffer = _KERNEL32.FlushConsoleInputBuffer
FlushConsoleInputBuffer.argtypes = [HANDLE]
FlushConsoleInputBuffer.restype = BOOL
WaitForSingleObject = _KERNEL32.WaitForSingleObject
WaitForSingleObject.argtypes = [HANDLE, DWORD]
WaitForSingleObject.restype = DWORD
OutHandle = GetStdHandle(STD_OUTPUT_HANDLE)
InHandle = GetStdHandle(STD_INPUT_HANDLE)
else:
@@ -610,9 +721,10 @@ else:
GetStdHandle = _win_only
GetConsoleScreenBufferInfo = _win_only
ScrollConsoleScreenBuffer = _win_only
GetConsoleMode = _win_only
SetConsoleMode = _win_only
ReadConsoleInput = _win_only
GetNumberOfConsoleInputEvents = _win_only
FlushConsoleInputBuffer = _win_only
WaitForSingleObject = _win_only
OutHandle = 0
InHandle = 0
+42
View File
@@ -0,0 +1,42 @@
"""
Windows event and VT sequence scanner
"""
from .base_eventqueue import BaseEventQueue
# Reference: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#input-sequences
VT_MAP: dict[bytes, str] = {
b'\x1b[A': 'up',
b'\x1b[B': 'down',
b'\x1b[C': 'right',
b'\x1b[D': 'left',
b'\x1b[1;5D': 'ctrl left',
b'\x1b[1;5C': 'ctrl right',
b'\x1b[H': 'home',
b'\x1b[F': 'end',
b'\x7f': 'backspace',
b'\x1b[2~': 'insert',
b'\x1b[3~': 'delete',
b'\x1b[5~': 'page up',
b'\x1b[6~': 'page down',
b'\x1bOP': 'f1',
b'\x1bOQ': 'f2',
b'\x1bOR': 'f3',
b'\x1bOS': 'f4',
b'\x1b[15~': 'f5',
b'\x1b[17~': 'f6',
b'\x1b[18~': 'f7',
b'\x1b[19~': 'f8',
b'\x1b[20~': 'f9',
b'\x1b[21~': 'f10',
b'\x1b[23~': 'f11',
b'\x1b[24~': 'f12',
}
class EventQueue(BaseEventQueue):
def __init__(self, encoding: str) -> None:
BaseEventQueue.__init__(self, encoding, VT_MAP)
+18 -12
View File
@@ -86,22 +86,27 @@ class REPLThread(threading.Thread):
global return_code
try:
banner = (
f'asyncio REPL {sys.version} on {sys.platform}\n'
f'Use "await" directly instead of "asyncio.run()".\n'
f'Type "help", "copyright", "credits" or "license" '
f'for more information.\n'
)
if not sys.flags.quiet:
banner = (
f'asyncio REPL {sys.version} on {sys.platform}\n'
f'Use "await" directly instead of "asyncio.run()".\n'
f'Type "help", "copyright", "credits" or "license" '
f'for more information.\n'
)
console.write(banner)
console.write(banner)
if startup_path := os.getenv("PYTHONSTARTUP"):
if not sys.flags.isolated and (startup_path := os.getenv("PYTHONSTARTUP")):
sys.audit("cpython.run_startup", startup_path)
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
try:
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
exec(startup_code, console.locals)
except SystemExit:
raise
except BaseException:
console.showtraceback()
ps1 = getattr(sys, "ps1", ">>> ")
if CAN_USE_PYREPL:
@@ -236,4 +241,5 @@ if __name__ == '__main__':
break
console.write('exiting asyncio REPL...\n')
loop.close()
sys.exit(return_code)
+29 -19
View File
@@ -483,10 +483,10 @@ class BaseEventLoop(events.AbstractEventLoop):
If factory is None the default task factory will be set.
If factory is a callable, it should have a signature matching
'(loop, coro, **kwargs)', where 'loop' will be a reference to the active
event loop, 'coro' will be a coroutine object, and **kwargs will be
arbitrary keyword arguments that should be passed on to Task.
The callable must return a Task.
'(loop, coro, **kwargs)', where 'loop' will be a reference to the
active event loop, 'coro' will be a coroutine object, and **kwargs
will be arbitrary keyword arguments that should be passed on to
Task. The callable must return a Task.
"""
if factory is not None and not callable(factory):
raise TypeError('task factory must be a callable or None')
@@ -721,8 +721,8 @@ class BaseEventLoop(events.AbstractEventLoop):
def stop(self):
"""Stop running the event loop.
Every callback already scheduled will still run. This simply informs
run_forever to stop looping after a complete iteration.
Every callback already scheduled will still run. This simply
informs run_forever to stop looping after a complete iteration.
"""
self._stopping = True
@@ -963,7 +963,7 @@ class BaseEventLoop(events.AbstractEventLoop):
f"and file {file!r} combination")
async def _sock_sendfile_fallback(self, sock, file, offset, count):
if offset:
if hasattr(file, 'seek'):
file.seek(offset)
blocksize = (
min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE)
@@ -1070,12 +1070,12 @@ class BaseEventLoop(events.AbstractEventLoop):
Create a streaming transport connection to a given internet host and
port: socket family AF_INET or socket.AF_INET6 depending on host (or
family if specified), socket type SOCK_STREAM. protocol_factory must be
a callable returning a protocol instance.
family if specified), socket type SOCK_STREAM. protocol_factory must
be a callable returning a protocol instance.
This method is a coroutine which will try to establish the connection
in the background. When successful, the coroutine returns a
(transport, protocol) pair.
This method is a coroutine which will try to establish the
connection in the background. When successful, the coroutine
returns a (transport, protocol) pair.
"""
if server_hostname is not None and not ssl:
raise ValueError('server_hostname is only meaningful with ssl')
@@ -1278,7 +1278,6 @@ class BaseEventLoop(events.AbstractEventLoop):
raise RuntimeError(
f"fallback is disabled and native sendfile is not "
f"supported for transport {transport!r}")
return await self._sendfile_fallback(transport, file,
offset, count)
@@ -1287,7 +1286,7 @@ class BaseEventLoop(events.AbstractEventLoop):
"sendfile syscall is not supported")
async def _sendfile_fallback(self, transp, file, offset, count):
if offset:
if hasattr(file, 'seek'):
file.seek(offset)
blocksize = min(count, 16384) if count else 16384
buf = bytearray(blocksize)
@@ -1345,6 +1344,17 @@ class BaseEventLoop(events.AbstractEventLoop):
# have a chance to get called before "ssl_protocol.connection_made()".
transport.pause_reading()
# gh-142352: move buffered StreamReader data to SSLProtocol
if server_side:
from .streams import StreamReaderProtocol
if isinstance(protocol, StreamReaderProtocol):
stream_reader = getattr(protocol, '_stream_reader', None)
if stream_reader is not None:
buffer = stream_reader._buffer
if buffer:
ssl_protocol._incoming.write(buffer)
buffer.clear()
transport.set_protocol(ssl_protocol)
conmade_cb = self.call_soon(ssl_protocol.connection_made, transport)
resume_cb = self.call_soon(transport.resume_reading)
@@ -1532,11 +1542,11 @@ class BaseEventLoop(events.AbstractEventLoop):
The host parameter can be a string, in that case the TCP server is
bound to host and port.
The host parameter can also be a sequence of strings and in that case
the TCP server is bound to all hosts of the sequence. If a host
appears multiple times (possibly indirectly e.g. when hostnames
resolve to the same IP address), the server is only bound once to that
host.
The host parameter can also be a sequence of strings and in that
case the TCP server is bound to all hosts of the sequence. If
a host appears multiple times (possibly indirectly e.g. when
hostnames resolve to the same IP address), the server is only bound
once to that host.
Return a Server object which can be used to stop the service.
+2 -2
View File
@@ -265,7 +265,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
# to avoid hanging forever in self._wait as otherwise _exit_waiters
# would never be woken up, we wake them up here.
for waiter in self._exit_waiters:
if not waiter.cancelled():
if not waiter.done():
waiter.set_result(self._returncode)
if all(p is not None and p.disconnected
for p in self._pipes.values()):
@@ -278,7 +278,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
finally:
# wake up futures waiting for wait()
for waiter in self._exit_waiters:
if not waiter.cancelled():
if not waiter.done():
waiter.set_result(self._returncode)
self._exit_waiters = None
self._loop = None
+1 -1
View File
@@ -18,8 +18,8 @@ _is_coroutine = object()
def iscoroutinefunction(func):
import warnings
"""Return True if func is a decorated coroutine function."""
import warnings
warnings._deprecated("asyncio.iscoroutinefunction",
f"{warnings._DEPRECATED_MSG}; "
"use inspect.iscoroutinefunction() instead",
+12 -9
View File
@@ -374,8 +374,8 @@ class AbstractEventLoop:
If host is an empty string or None all interfaces are assumed
and a list of multiple sockets will be returned (most likely
one for IPv4 and another one for IPv6). The host parameter can also be
a sequence (e.g. list) of hosts to bind to.
one for IPv4 and another one for IPv6). The host parameter can also
be a sequence (e.g. list) of hosts to bind to.
family can be set to either AF_INET or AF_INET6 to force the
socket to use IPv4 or IPv6. If not set it will be determined
@@ -415,8 +415,9 @@ class AbstractEventLoop:
start_serving set to True (default) causes the created server
to start accepting connections immediately. When set to False,
the user should await Server.start_serving() or Server.serve_forever()
to make the server to start accepting connections.
the user should await Server.start_serving() or
Server.serve_forever() to make the server to start accepting
connections.
"""
raise NotImplementedError
@@ -479,8 +480,9 @@ class AbstractEventLoop:
start_serving set to True (default) causes the created server
to start accepting connections immediately. When set to False,
the user should await Server.start_serving() or Server.serve_forever()
to make the server to start accepting connections.
the user should await Server.start_serving() or
Server.serve_forever() to make the server to start accepting
connections.
"""
raise NotImplementedError
@@ -511,8 +513,8 @@ class AbstractEventLoop:
protocol_factory must be a callable returning a protocol instance.
socket family AF_INET, socket.AF_INET6 or socket.AF_UNIX depending on
host (or family if specified), socket type SOCK_DGRAM.
socket family AF_INET, socket.AF_INET6 or socket.AF_UNIX depending
on host (or family if specified), socket type SOCK_DGRAM.
reuse_address tells the kernel to reuse a local socket in
TIME_WAIT state, without waiting for its natural timeout to
@@ -552,7 +554,8 @@ class AbstractEventLoop:
async def connect_write_pipe(self, protocol_factory, pipe):
"""Register write pipe in event loop.
protocol_factory should instantiate object with BaseProtocol interface.
protocol_factory should instantiate object with BaseProtocol
interface.
Pipe is file-like object already switched to nonblocking.
Return pair (transport, protocol), where transport support
WriteTransport interface."""
+2 -2
View File
@@ -392,7 +392,7 @@ def _chain_future(source, destination):
def _call_check_cancel(destination):
if destination.cancelled():
if source_loop is None or source_loop is dest_loop:
if source_loop is None or source_loop is events._get_running_loop():
source.cancel()
else:
source_loop.call_soon_threadsafe(source.cancel)
@@ -401,7 +401,7 @@ def _chain_future(source, destination):
if (destination.cancelled() and
dest_loop is not None and dest_loop.is_closed()):
return
if dest_loop is None or dest_loop is source_loop:
if dest_loop is None or dest_loop is events._get_running_loop():
_set_state(destination, source)
else:
if dest_loop.is_closed():
+7 -7
View File
@@ -112,13 +112,13 @@ def capture_call_graph(
optional keyword-only 'depth' argument can be used to skip the specified
number of frames from top of the stack.
If the optional keyword-only 'limit' argument is provided, each call stack
in the resulting graph is truncated to include at most ``abs(limit)``
entries. If 'limit' is positive, the entries left are the closest to
the invocation point. If 'limit' is negative, the topmost entries are
left. If 'limit' is omitted or None, all entries are present.
If 'limit' is 0, the call stack is not captured at all, only
"awaited by" information is present.
If the optional keyword-only 'limit' argument is provided, each call
stack in the resulting graph is truncated to include at most
``abs(limit)`` entries. If 'limit' is positive, the entries left are
the closest to the invocation point. If 'limit' is negative, the
topmost entries are left. If 'limit' is omitted or None, all entries
are present. If 'limit' is 0, the call stack is not captured at all,
only "awaited by" information is present.
"""
loop = events._get_running_loop()
+9 -9
View File
@@ -158,10 +158,10 @@ class Lock(_ContextManagerMixin, mixins._LoopBoundMixin):
class Event(mixins._LoopBoundMixin):
"""Asynchronous equivalent to threading.Event.
Class implementing event objects. An event manages a flag that can be set
to true with the set() method and reset to false with the clear() method.
The wait() method blocks until the flag is true. The flag is initially
false.
Class implementing event objects. An event manages a flag that can be
set to true with the set() method and reset to false with the clear()
method. The wait() method blocks until the flag is true. The flag is
initially false.
"""
def __init__(self):
@@ -353,9 +353,9 @@ class Semaphore(_ContextManagerMixin, mixins._LoopBoundMixin):
"""A Semaphore implementation.
A semaphore manages an internal counter which is decremented by each
acquire() call and incremented by each release() call. The counter
can never go below zero; when acquire() finds that it is zero, it blocks,
waiting until some other thread calls release().
acquire() call and incremented by each release() call. The counter
can never go below zero; when acquire() finds that it is zero, it
blocks, waiting until some other thread calls release().
Semaphores also support the context management protocol.
@@ -511,8 +511,8 @@ class Barrier(mixins._LoopBoundMixin):
async def wait(self):
"""Wait for the barrier.
When the specified number of tasks have started waiting, they are all
simultaneously awoken.
When the specified number of tasks have started waiting, they are
all simultaneously awoken.
Returns an unique and individual index number from 0 to 'parties-1'.
"""
async with self._cond:
+1 -2
View File
@@ -756,8 +756,7 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
offset += blocksize
total_sent += blocksize
finally:
if total_sent > 0:
file.seek(offset)
file.seek(offset)
async def _sendfile_native(self, transp, file, offset, count):
resume_reading = transp.is_reading()
+20 -18
View File
@@ -33,11 +33,11 @@ class QueueShutDown(Exception):
class Queue(mixins._LoopBoundMixin):
"""A queue, useful for coordinating producer and consumer coroutines.
If maxsize is less than or equal to zero, the queue size is infinite. If it
is an integer greater than 0, then "await put()" will block when the
queue reaches maxsize, until an item is removed by get().
If maxsize is less than or equal to zero, the queue size is infinite.
If it is an integer greater than 0, then "await put()" will block when
the queue reaches maxsize, until an item is removed by get().
Unlike the standard library Queue, you can reliably know this Queue's size
Unlike queue.Queue, you can reliably know this Queue's size
with qsize(), since your single-threaded asyncio application won't be
interrupted between calling qsize() and doing an operation on the Queue.
"""
@@ -174,8 +174,8 @@ class Queue(mixins._LoopBoundMixin):
If queue is empty, wait until an item is available.
Raises QueueShutDown if the queue has been shut down and is empty, or
if the queue has been shut down immediately.
Raises QueueShutDown if the queue has been shut down and is empty,
or if the queue has been shut down immediately.
"""
while self.empty():
if self._is_shutdown and self.empty():
@@ -203,10 +203,11 @@ class Queue(mixins._LoopBoundMixin):
def get_nowait(self):
"""Remove and return an item from the queue.
Return an item if one is immediately available, else raise QueueEmpty.
Return an item if one is immediately available, else raise
QueueEmpty.
Raises QueueShutDown if the queue has been shut down and is empty, or
if the queue has been shut down immediately.
Raises QueueShutDown if the queue has been shut down and is empty,
or if the queue has been shut down immediately.
"""
if self.empty():
if self._is_shutdown:
@@ -223,12 +224,12 @@ class Queue(mixins._LoopBoundMixin):
a subsequent call to task_done() tells the queue that the processing
on the task is complete.
If a join() is currently blocking, it will resume when all items have
been processed (meaning that a task_done() call was received for every
item that had been put() into the queue).
If a join() is currently blocking, it will resume when all items
have been processed (meaning that a task_done() call was received
for every item that had been put() into the queue).
Raises ValueError if called more times than there were items placed in
the queue.
Raises ValueError if called more times than there were items placed
in the queue.
"""
if self._unfinished_tasks <= 0:
raise ValueError('task_done() called too many times')
@@ -239,10 +240,11 @@ class Queue(mixins._LoopBoundMixin):
async def join(self):
"""Block until all items in the queue have been gotten and processed.
The count of unfinished tasks goes up whenever an item is added to the
queue. The count goes down whenever a consumer calls task_done() to
indicate that the item was retrieved and all work on it is complete.
When the count of unfinished tasks drops to zero, join() unblocks.
The count of unfinished tasks goes up whenever an item is added to
the queue. The count goes down whenever a consumer calls
task_done() to indicate that the item was retrieved and all work on
it is complete. When the count of unfinished tasks drops to zero,
join() unblocks.
"""
if self._unfinished_tasks > 0:
await self._finished.wait()
+2 -1
View File
@@ -35,7 +35,8 @@ class Runner:
with asyncio.Runner(debug=True) as runner:
runner.run(main())
The run() method can be called multiple times within the runner's context.
The run() method can be called multiple times within the runner's
context.
This can be useful for interactive console (e.g. IPython),
unittest runners, console tools, -- everywhere when async code
+17 -14
View File
@@ -530,11 +530,12 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop):
async def sock_sendall(self, sock, data):
"""Send data to the socket.
The socket must be connected to a remote socket. This method continues
to send data from data until either all data has been sent or an
error occurs. None is returned on success. On error, an exception is
raised, and there is no way to determine how much data, if any, was
successfully processed by the receiving end of the connection.
The socket must be connected to a remote socket. This method
continues to send data from data until either all data has been
sent or an error occurs. None is returned on success. On error,
an exception is raised, and there is no way to determine how much
data, if any, was successfully processed by the receiving end of
the connection.
"""
base_events._check_ssl_socket(sock)
if self._debug and sock.gettimeout() != 0:
@@ -583,11 +584,12 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop):
async def sock_sendto(self, sock, data, address):
"""Send data to the socket.
The socket must be connected to a remote socket. This method continues
to send data from data until either all data has been sent or an
error occurs. None is returned on success. On error, an exception is
raised, and there is no way to determine how much data, if any, was
successfully processed by the receiving end of the connection.
The socket must be connected to a remote socket. This method
continues to send data from data until either all data has been
sent or an error occurs. None is returned on success. On error,
an exception is raised, and there is no way to determine how much
data, if any, was successfully processed by the receiving end of
the connection.
"""
base_events._check_ssl_socket(sock)
if self._debug and sock.gettimeout() != 0:
@@ -698,10 +700,11 @@ class BaseSelectorEventLoop(base_events.BaseEventLoop):
async def sock_accept(self, sock):
"""Accept a connection.
The socket must be bound to an address and listening for connections.
The return value is a pair (conn, address) where conn is a new socket
object usable to send and receive data on the connection, and address
is the address bound to the socket on the other end of the connection.
The socket must be bound to an address and listening for
connections. The return value is a pair (conn, address) where
conn is a new socket object usable to send and receive data on the
connection, and address is the address bound to the socket on the
other end of the connection.
"""
base_events._check_ssl_socket(sock)
if self._debug and sock.gettimeout() != 0:
+7 -7
View File
@@ -541,17 +541,17 @@ class StreamReader:
self._waiter = None
async def readline(self):
"""Read chunk of data from the stream until newline (b'\n') is found.
r"""Read chunk of data from the stream until newline (b'\n') is found.
On success, return chunk that ends with newline. If only partial
On success, return chunk that ends with newline. If only partial
line can be read due to EOF, return incomplete line without
terminating newline. When EOF was reached while no bytes read, empty
bytes object is returned.
terminating newline. When EOF was reached while no bytes read,
empty bytes object is returned.
If limit is reached, ValueError will be raised. In that case, if
If limit is reached, ValueError will be raised. In that case, if
newline was found, complete line including newline will be removed
from internal buffer. Else, internal buffer will be cleared. Limit is
compared against part of the line without newline.
from internal buffer. Else, internal buffer will be cleared.
Limit is compared against part of the line without newline.
If stream was paused, this function will automatically resume it if
needed.
+30 -27
View File
@@ -624,10 +624,11 @@ def as_completed(fs, *, timeout=None):
Run the supplied awaitables concurrently. The returned object can be
iterated to obtain the results of the awaitables as they finish.
The object returned can be iterated as an asynchronous iterator or a plain
iterator. When asynchronous iteration is used, the originally-supplied
awaitables are yielded if they are tasks or futures. This makes it easy to
correlate previously-scheduled tasks with their results:
The object returned can be iterated as an asynchronous iterator or
a plain iterator. When asynchronous iteration is used, the
originally-supplied awaitables are yielded if they are tasks or
futures. This makes it easy to correlate previously-scheduled tasks
with their results:
ipv4_connect = create_task(open_connection("127.0.0.1", 80))
ipv6_connect = create_task(open_connection("::1", 80))
@@ -643,26 +644,27 @@ def as_completed(fs, *, timeout=None):
else:
print("IPv4 connection established.")
During asynchronous iteration, implicitly-created tasks will be yielded for
supplied awaitables that aren't tasks or futures.
During asynchronous iteration, implicitly-created tasks will be
yielded for supplied awaitables that aren't tasks or futures.
When used as a plain iterator, each iteration yields a new coroutine that
returns the result or raises the exception of the next completed awaitable.
This pattern is compatible with Python versions older than 3.13:
When used as a plain iterator, each iteration yields a new coroutine
that returns the result or raises the exception of the next completed
awaitable. This pattern is compatible with Python versions older than
3.13:
ipv4_connect = create_task(open_connection("127.0.0.1", 80))
ipv6_connect = create_task(open_connection("::1", 80))
tasks = [ipv4_connect, ipv6_connect]
for next_connect in as_completed(tasks):
# next_connect is not one of the original task objects. It must be
# awaited to obtain the result value or raise the exception of the
# awaitable that finishes next.
# next_connect is not one of the original task objects. It must
# be awaited to obtain the result value or raise the exception
# of the awaitable that finishes next.
reader, writer = await next_connect
A TimeoutError is raised if the timeout occurs before all awaitables are
done. This is raised by the async for loop during asynchronous iteration or
by the coroutines yielded during plain iteration.
A TimeoutError is raised if the timeout occurs before all awaitables
are done. This is raised by the async for loop during asynchronous
iteration or by the coroutines yielded during plain iteration.
"""
if inspect.isawaitable(fs):
raise TypeError(
@@ -1030,21 +1032,22 @@ def run_coroutine_threadsafe(coro, loop):
def create_eager_task_factory(custom_task_constructor):
"""Create a function suitable for use as a task factory on an event-loop.
Example usage:
Example usage:
loop.set_task_factory(
asyncio.create_eager_task_factory(my_task_constructor))
loop.set_task_factory(
asyncio.create_eager_task_factory(my_task_constructor))
Now, tasks created will be started immediately (rather than being first
scheduled to an event loop). The constructor argument can be any callable
that returns a Task-compatible object and has a signature compatible
with `Task.__init__`; it must have the `eager_start` keyword argument.
Now, tasks created will be started immediately (rather than being first
scheduled to an event loop). The constructor argument can be any
callable that returns a Task-compatible object and has a signature
compatible with `Task.__init__`; it must have the `eager_start`
keyword argument.
Most applications will use `Task` for `custom_task_constructor` and in
this case there's no need to call `create_eager_task_factory()`
directly. Instead the global `eager_task_factory` instance can be
used. E.g. `loop.set_task_factory(asyncio.eager_task_factory)`.
"""
Most applications will use `Task` for `custom_task_constructor` and in
this case there's no need to call `create_eager_task_factory()`
directly. Instead the global `eager_task_factory` instance can be
used. E.g. `loop.set_task_factory(asyncio.eager_task_factory)`.
"""
def factory(loop, coro, *, eager_start=True, **kwargs):
return custom_task_constructor(
+2 -1
View File
@@ -17,7 +17,8 @@ async def to_thread(func, /, *args, **kwargs):
allowing context variables from the main thread to be accessed in the
separate thread.
Return a coroutine that can be awaited to get the eventual result of *func*.
Return a coroutine that can be awaited to get the eventual result of
*func*.
"""
loop = events.get_running_loop()
ctx = contextvars.copy_context()
+2 -1
View File
@@ -25,7 +25,8 @@ class _State(enum.Enum):
class Timeout:
"""Asynchronous context manager for cancelling overdue coroutines.
Use `timeout()` or `timeout_at()` rather than instantiating this class directly.
Use `timeout()` or `timeout_at()` rather than instantiating this class
directly.
"""
def __init__(self, when: float | None) -> None:
+11 -10
View File
@@ -54,7 +54,8 @@ def waitstatus_to_exitcode(status):
class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
"""Unix event loop.
Adds signal handling and UNIX Domain Socket support to SelectorEventLoop.
Adds signal handling and UNIX Domain Socket support to
SelectorEventLoop.
"""
def __init__(self, selector=None):
@@ -384,12 +385,12 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
# order to simplify the common case.
self.remove_writer(registered_fd)
if fut.cancelled():
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
self._sock_sendfile_update_filepos(fileno, offset)
return
if count:
blocksize = count - total_sent
if blocksize <= 0:
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
self._sock_sendfile_update_filepos(fileno, offset)
fut.set_result(total_sent)
return
@@ -423,20 +424,20 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
# plain send().
err = exceptions.SendfileNotAvailableError(
"os.sendfile call failed")
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
self._sock_sendfile_update_filepos(fileno, offset)
fut.set_exception(err)
else:
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
self._sock_sendfile_update_filepos(fileno, offset)
fut.set_exception(exc)
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
self._sock_sendfile_update_filepos(fileno, offset)
fut.set_exception(exc)
else:
if sent == 0:
# EOF
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
self._sock_sendfile_update_filepos(fileno, offset)
fut.set_result(total_sent)
else:
offset += sent
@@ -447,9 +448,9 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
fd, sock, fileno,
offset, count, blocksize, total_sent)
def _sock_sendfile_update_filepos(self, fileno, offset, total_sent):
if total_sent > 0:
os.lseek(fileno, offset, os.SEEK_SET)
def _sock_sendfile_update_filepos(self, fileno, offset):
# After this helper runs, the source fd's lseek pointer is at offset."
os.lseek(fileno, offset, os.SEEK_SET)
def _sock_add_cancellation_callback(self, fut, sock):
def cb(fut):
+3
View File
@@ -610,6 +610,9 @@ class IocpProactor:
ov = _overlapped.Overlapped(NULL)
offset_low = offset & 0xffff_ffff
offset_high = (offset >> 32) & 0xffff_ffff
# TransmitFile ignores OVERLAPPED.Offset for handles not opened with
# FILE_FLAG_OVERLAPPED, so seek the CRT file pointer to match.
file.seek(offset)
ov.TransmitFile(sock.fileno(),
msvcrt.get_osfhandle(file.fileno()),
offset_low, offset_high,
+17 -9
View File
@@ -10,7 +10,6 @@ import itertools
import msvcrt
import os
import subprocess
import tempfile
import warnings
@@ -24,6 +23,7 @@ BUFSIZE = 8192
PIPE = subprocess.PIPE
STDOUT = subprocess.STDOUT
_mmap_counter = itertools.count()
_MAX_PIPE_ATTEMPTS = 20
# Replacement for os.pipe() using handles instead of fds
@@ -31,10 +31,6 @@ _mmap_counter = itertools.count()
def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE):
"""Like os.pipe() but with overlapped support and using handles not fds."""
address = tempfile.mktemp(
prefix=r'\\.\pipe\python-pipe-{:d}-{:d}-'.format(
os.getpid(), next(_mmap_counter)))
if duplex:
openmode = _winapi.PIPE_ACCESS_DUPLEX
access = _winapi.GENERIC_READ | _winapi.GENERIC_WRITE
@@ -56,9 +52,20 @@ def pipe(*, duplex=False, overlapped=(True, True), bufsize=BUFSIZE):
h1 = h2 = None
try:
h1 = _winapi.CreateNamedPipe(
address, openmode, _winapi.PIPE_WAIT,
1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL)
for attempts in itertools.count():
address = r'\\.\pipe\python-pipe-{:d}-{:d}-{}'.format(
os.getpid(), next(_mmap_counter), os.urandom(8).hex())
try:
h1 = _winapi.CreateNamedPipe(
address, openmode, _winapi.PIPE_WAIT,
1, obsize, ibsize, _winapi.NMPWAIT_WAIT_FOREVER, _winapi.NULL)
break
except OSError as e:
if attempts >= _MAX_PIPE_ATTEMPTS:
raise
if e.winerror not in (_winapi.ERROR_PIPE_BUSY,
_winapi.ERROR_ACCESS_DENIED):
raise
h2 = _winapi.CreateFile(
address, access, 0, _winapi.NULL, _winapi.OPEN_EXISTING,
@@ -104,8 +111,9 @@ class PipeHandle:
def close(self, *, CloseHandle=_winapi.CloseHandle):
if self._handle is not None:
CloseHandle(self._handle)
handle = self._handle
self._handle = None
CloseHandle(handle)
def __del__(self, _warn=warnings.warn):
if self._handle is not None:
+22 -15
View File
@@ -325,17 +325,20 @@ def a85encode(b, *, foldspaces=False, wrapcol=0, pad=False, adobe=False):
foldspaces is an optional flag that uses the special short sequence 'y'
instead of 4 consecutive spaces (ASCII 0x20) as supported by 'btoa'. This
feature is not supported by the "standard" Adobe encoding.
feature is not supported by the standard encoding used in PDF.
wrapcol controls whether the output should have newline (b'\\n') characters
added to it. If this is non-zero, each output line will be at most this
many characters long, excluding the trailing newline.
If wrapcol is non-zero, insert a newline (b'\\n') character after at most
every wrapcol characters.
pad controls whether the input is padded to a multiple of 4 before
encoding. Note that the btoa implementation always pads.
pad controls whether zero-padding applied to the end of the input
is fully retained in the output encoding, as done by btoa,
producing an exact multiple of 5 bytes of output.
adobe controls whether the encoded byte sequence is framed with <~
and ~>, as in a PostScript base-85 string literal. Note that
while ASCII85Decode streams in PDF documents must be terminated
with ~>, they must not use a leading <~.
adobe controls whether the encoded byte sequence is framed with <~ and ~>,
which is used by the Adobe implementation.
"""
global _a85chars, _a85chars2
# Delay the initialization of tables to not waste memory
@@ -364,12 +367,14 @@ def a85encode(b, *, foldspaces=False, wrapcol=0, pad=False, adobe=False):
def a85decode(b, *, foldspaces=False, adobe=False, ignorechars=b' \t\n\r\v'):
"""Decode the Ascii85 encoded bytes-like object or ASCII string b.
foldspaces is a flag that specifies whether the 'y' short sequence should be
accepted as shorthand for 4 consecutive spaces (ASCII 0x20). This feature is
not supported by the "standard" Adobe encoding.
foldspaces is a flag that specifies whether the 'y' short sequence
should be accepted as shorthand for 4 consecutive spaces (ASCII
0x20). This feature is not supported by the standard Ascii85
encoding used in PDF and PostScript.
adobe controls whether the input sequence is in Adobe Ascii85 format (i.e.
is framed with <~ and ~>).
adobe controls whether the <~ and ~> markers are present. While
the leading <~ is not required, the input must end with ~>, or a
ValueError is raised.
ignorechars should be a byte string containing characters to ignore from the
input. This should only contain whitespace characters, and by default
@@ -442,8 +447,10 @@ _b85dec = None
def b85encode(b, pad=False):
"""Encode bytes-like object b in base85 format and return a bytes object.
If pad is true, the input is padded with b'\\0' so its length is a multiple of
4 bytes before encoding.
The input is padded with b'\0' so its length is a multiple of 4
bytes before encoding. If pad is true, all the resulting
characters are retained in the output, which will always be a
multiple of 5 bytes.
"""
global _b85chars, _b85chars2
# Delay the initialization of tables to not waste memory
+1 -2
View File
@@ -57,8 +57,7 @@ except ImportError:
try:
from _collections import defaultdict
except ImportError:
# TODO: RUSTPYTHON - implement defaultdict in Rust
from ._defaultdict import defaultdict
pass
heapq = None # Lazily imported
-62
View File
@@ -1,62 +0,0 @@
from reprlib import recursive_repr as _recursive_repr
class defaultdict(dict):
def __init__(self, *args, **kwargs):
if len(args) >= 1:
default_factory = args[0]
if default_factory is not None and not callable(default_factory):
raise TypeError("first argument must be callable or None")
args = args[1:]
else:
default_factory = None
super().__init__(*args, **kwargs)
self.default_factory = default_factory
def __missing__(self, key):
if self.default_factory is not None:
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
@_recursive_repr()
def __repr_factory(factory):
return repr(factory)
def __repr__(self):
return f"{type(self).__name__}({defaultdict.__repr_factory(self.default_factory)}, {dict.__repr__(self)})"
def copy(self):
return type(self)(self.default_factory, self)
__copy__ = copy
def __reduce__(self):
if self.default_factory is not None:
args = self.default_factory,
else:
args = ()
return type(self), args, None, None, iter(self.items())
def __or__(self, other):
if not isinstance(other, dict):
return NotImplemented
new = defaultdict(self.default_factory, self)
new.update(other)
return new
def __ror__(self, other):
if not isinstance(other, dict):
return NotImplemented
new = defaultdict(self.default_factory, other)
new.update(self)
return new
defaultdict.__module__ = 'collections'
+15 -13
View File
@@ -61,8 +61,9 @@ class FrameInfo:
def get_frame_info(frame_buffer):
"""Get Zstandard frame information from a frame header.
*frame_buffer* is a bytes-like object. It should start from the beginning
of a frame, and needs to include at least the frame header (6 to 18 bytes).
*frame_buffer* is a bytes-like object. It should start from the
beginning of a frame, and needs to include at least the frame header
(6 to 18 bytes).
The returned FrameInfo object has two attributes.
'decompressed_size' is the size in bytes of the data in the frame when
@@ -103,16 +104,17 @@ def finalize_dict(zstd_dict, /, samples, dict_size, level):
finalize *zstd_dict* by adding headers and statistics according to the
Zstandard dictionary format.
You may compose an effective dictionary content by hand, which is used as
basis dictionary, and use some samples to finalize a dictionary. The basis
dictionary may be a "raw content" dictionary. See *is_raw* in ZstdDict.
You may compose an effective dictionary content by hand, which is used
as basis dictionary, and use some samples to finalize a dictionary. The
basis dictionary may be a "raw content" dictionary. See *is_raw* in
ZstdDict.
*samples* is an iterable of samples, where a sample is a bytes-like object
representing a file.
*samples* is an iterable of samples, where a sample is a bytes-like
object representing a file.
*dict_size* is the dictionary's maximum size, in bytes.
*level* is the expected compression level. The statistics for each
compression level differ, so tuning the dictionary to the compression level
can provide improvements.
compression level differ, so tuning the dictionary to the compression
level can provide improvements.
"""
if not isinstance(zstd_dict, ZstdDict):
@@ -140,8 +142,8 @@ def compress(data, level=None, options=None, zstd_dict=None):
COMPRESSION_LEVEL_DEFAULT ('3').
*options* is a dict object that contains advanced compression
parameters. See CompressionParameter for more on options.
*zstd_dict* is a ZstdDict object, a pre-trained Zstandard dictionary. See
the function train_dict for how to train a ZstdDict on sample data.
*zstd_dict* is a ZstdDict object, a pre-trained Zstandard dictionary.
See the function train_dict for how to train a ZstdDict on sample data.
For incremental compression, use a ZstdCompressor instead.
"""
@@ -152,8 +154,8 @@ def compress(data, level=None, options=None, zstd_dict=None):
def decompress(data, zstd_dict=None, options=None):
"""Decompress one or more frames of Zstandard compressed *data*.
*zstd_dict* is a ZstdDict object, a pre-trained Zstandard dictionary. See
the function train_dict for how to train a ZstdDict on sample data.
*zstd_dict* is a ZstdDict object, a pre-trained Zstandard dictionary.
See the function train_dict for how to train a ZstdDict on sample data.
*options* is a dict object that contains advanced compression
parameters. See DecompressionParameter for more on options.
+19 -18
View File
@@ -36,9 +36,9 @@ class ZstdFile(_streams.BaseStream):
*file* can be either an file-like object, or a file name to open.
*mode* can be 'r' for reading (default), 'w' for (over)writing, 'x' for
creating exclusively, or 'a' for appending. These can equivalently be
given as 'rb', 'wb', 'xb' and 'ab' respectively.
*mode* can be 'r' for reading (default), 'w' for (over)writing, 'x'
for creating exclusively, or 'a' for appending. These can
equivalently be given as 'rb', 'wb', 'xb' and 'ab' respectively.
*level* is an optional int specifying the compression level to use,
or COMPRESSION_LEVEL_DEFAULT if not given.
@@ -296,26 +296,27 @@ def open(file, /, mode='rb', *, level=None, options=None, zstd_dict=None,
encoding=None, errors=None, newline=None):
"""Open a Zstandard compressed file in binary or text mode.
file can be either a file name (given as a str, bytes, or PathLike object),
in which case the named file is opened, or it can be an existing file object
to read from or write to.
file can be either a file name (given as a str, bytes, or PathLike
object), in which case the named file is opened, or it can be
an existing file object to read from or write to.
The mode parameter can be 'r', 'rb' (default), 'w', 'wb', 'x', 'xb', 'a',
'ab' for binary mode, or 'rt', 'wt', 'xt', 'at' for text mode.
The mode parameter can be 'r', 'rb' (default), 'w', 'wb', 'x', 'xb',
'a', 'ab' for binary mode, or 'rt', 'wt', 'xt', 'at' for text mode.
The level, options, and zstd_dict parameters specify the settings the same
as ZstdFile.
The level, options, and zstd_dict parameters specify the settings the
same as ZstdFile.
When using read mode (decompression), the options parameter is a dict
representing advanced decompression options. The level parameter is not
supported in this case. When using write mode (compression), only one of
level, an int representing the compression level, or options, a dict
representing advanced compression options, may be passed. In both modes,
zstd_dict is a ZstdDict instance containing a trained Zstandard dictionary.
representing advanced decompression options. The level parameter is not
supported in this case. When using write mode (compression), only one
of level, an int representing the compression level, or options, a dict
representing advanced compression options, may be passed. In both
modes, zstd_dict is a ZstdDict instance containing a trained Zstandard
dictionary.
For binary mode, this function is equivalent to the ZstdFile constructor:
ZstdFile(filename, mode, ...). In this case, the encoding, errors and
newline parameters must not be provided.
For binary mode, this function is equivalent to the ZstdFile
constructor: ZstdFile(filename, mode, ...). In this case, the encoding,
errors and newline parameters must not be provided.
For text mode, an ZstdFile object is created, and wrapped in an
io.TextIOWrapper instance with the specified encoding, error handling
+10
View File
@@ -1463,6 +1463,16 @@ def get_phrase(value):
else:
try:
token, value = get_word(value)
if (token[0].token_type == 'encoded-word'
and phrase
and phrase[-1].token_type == 'atom'
and len(phrase[-1]) > 1
and phrase[-1][-2].token_type == 'encoded-word'
and phrase[-1][-1].token_type == 'cfws'
and not phrase[-1][-1].comments
):
# linear ws between ews needs special handing...
phrase[-1][-1] = EWWhiteSpaceTerminal(phrase[-1], 'fws')
except errors.HeaderParseError:
if value[0] in CFWS_LEADER:
token, value = get_cfws(value)
+1 -1
View File
@@ -59,7 +59,7 @@ def _parsedate_tz(data):
The last (additional) element is the time zone offset in seconds, except if
the timezone was specified as -0000. In that case the last element is
None. This indicates a UTC timestamp that explicitly declaims knowledge of
None. This indicates a UTC timestamp that explicitly disclaims knowledge of
the source timezone, as opposed to a +0000 timestamp that indicates the
source timezone really was UTC.
-2
View File
@@ -93,8 +93,6 @@ ALIASES = {
# Map charsets to their Unicode codec strings.
CODEC_MAP = {
'gb2312': 'eucgb2312_cn',
'big5': 'big5_tw',
# Hack: We don't want *any* conversion for stuff marked us-ascii, as all
# sorts of garbage might be sent to us in the guise of 7-bit us-ascii.
# Let that stuff pass through without conversion to/from Unicode.
+8
View File
@@ -71,6 +71,10 @@ aliases = {
# cp1140 codec
'1140' : 'cp1140',
'cp01140' : 'cp1140',
'csibm01140' : 'cp1140',
'ebcdic_us_37_euro' : 'cp1140',
'ibm01140' : 'cp1140',
'ibm1140' : 'cp1140',
# cp1250 codec
@@ -159,8 +163,12 @@ aliases = {
# cp858 codec
'858' : 'cp858',
'cp00858' : 'cp858',
'csibm00858' : 'cp858',
'csibm858' : 'cp858',
'ibm00858' : 'cp858',
'ibm858' : 'cp858',
'pc_multilingual_850_euro' : 'cp858',
# cp860 codec
'860' : 'cp860',
+1 -1
View File
@@ -10,7 +10,7 @@ from shutil import copy2
__all__ = ["version", "bootstrap"]
_PIP_VERSION = "26.1.1"
_PIP_VERSION = "26.1.2"
# Directory of system wheel packages. Some Linux distribution packaging
# policies recommend against bundling dependencies. For example, Fedora
Vendored
+13 -12
View File
@@ -678,9 +678,9 @@ class EnumType(type):
"""
Either returns an existing member, or creates a new enum class.
This method is used both when an enum class is given a value to match
to an enumeration member (i.e. Color(3)) and for the functional API
(i.e. Color = Enum('Color', names='RED GREEN BLUE')).
This method is used both when an enum class is given a value to
match to an enumeration member (i.e. Color(3)) and for the
functional API (i.e. Color = Enum('Color', names='RED GREEN BLUE')).
The value lookup branch is chosen if the enum is final.
@@ -688,16 +688,17 @@ class EnumType(type):
`value` will be the name of the new class.
`names` should be either a string of white-space/comma delimited names
(values will start at `start`), or an iterator/mapping of name, value pairs.
`names` should be either a string of white-space/comma delimited
names (values will start at `start`), or an iterator/mapping of
name, value pairs.
`module` should be set to the module this class is being created in;
if it is not set, an attempt to find that module will be made, but if
it fails the class will not be picklable.
if it is not set, an attempt to find that module will be made, but
if it fails the class will not be picklable.
`qualname` should be set to the actual location this class can be found
at in its module; by default it is set to the global scope. If this is
not correct, unpickling will fail in some circumstances.
`qualname` should be set to the actual location this class can be
found at in its module; by default it is set to the global scope.
If this is not correct, unpickling will fail in some circumstances.
`type`, if set, will be mixed in as the first base class.
"""
@@ -791,8 +792,8 @@ class EnumType(type):
"""
Returns a mapping of member name->value.
This mapping lists all enum members, including aliases. Note that this
is a read-only view of the internal mapping.
This mapping lists all enum members, including aliases. Note that
this is a read-only view of the internal mapping.
"""
return MappingProxyType(cls._member_map_)
+10 -1
View File
@@ -883,7 +883,16 @@ def ftpcp(source, sourcename, target, targetname = '', type = 'I'):
type = 'TYPE ' + type
source.voidcmd(type)
target.voidcmd(type)
sourcehost, sourceport = parse227(source.sendcmd('PASV'))
# Don't trust the IPv4 address the source server advertises in its PASV
# reply: a malicious source could otherwise point the target's data
# connection at an arbitrary host (SSRF). A caller that needs the old
# behavior can set trust_server_pasv_ipv4_address on the source FTP
# object. See FTP.makepasv(), which applies the same rule.
untrusted_host, sourceport = parse227(source.sendcmd('PASV'))
if source.trust_server_pasv_ipv4_address:
sourcehost = untrusted_host
else:
sourcehost = source.sock.getpeername()[0]
target.sendport(sourcehost, sourceport)
# RFC 959: the user must "listen" [...] BEFORE sending the
# transfer request.
+6 -6
View File
@@ -556,16 +556,16 @@ def lru_cache(maxsize=128, typed=False):
If *maxsize* is set to None, the LRU features are disabled and the cache
can grow without bound.
If *typed* is True, arguments of different types will be cached separately.
For example, f(decimal.Decimal("3.0")) and f(3.0) will be treated as
distinct calls with distinct results. Some types such as str and int may
be cached separately even when typed is false.
If *typed* is True, arguments of different types will be cached
separately. For example, f(decimal.Decimal("3.0")) and f(3.0) will be
treated as distinct calls with distinct results. Some types such as
str and int may be cached separately even when typed is false.
Arguments to the cached function must be hashable.
View the cache statistics named tuple (hits, misses, maxsize, currsize)
with f.cache_info(). Clear the cache and statistics with f.cache_clear().
Access the underlying function with f.__wrapped__.
with f.cache_info(). Clear the cache and statistics with
f.cache_clear(). Access the underlying function with f.__wrapped__.
See: https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
Vendored
+17 -17
View File
@@ -25,17 +25,17 @@ 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 `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 `include_hidden` is true, the patterns '*', '?', '**' will match
hidden directories.
If `recursive` is true, the pattern '**' will match any files and
zero or more directories and subdirectories.
@@ -56,16 +56,16 @@ def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
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`.
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 `include_hidden` is true, the patterns '*', '?', '**' will match
hidden directories.
If `recursive` is true, the pattern '**' will match any files and
zero or more directories and subdirectories.
@@ -294,15 +294,15 @@ _no_recurse_symlinks = object()
def translate(pat, *, recursive=False, include_hidden=False, seps=None):
"""Translate a pathname with shell wildcards to a regular expression.
If `recursive` is true, the pattern segment '**' will match any number of
path segments.
If `recursive` is true, the pattern segment '**' will match any number
of path segments.
If `include_hidden` is true, wildcards can match path segments beginning
with a dot ('.').
If a sequence of separator characters is given to `seps`, they will be
used to split the pattern into segments and match path separators. If not
given, os.path.sep and os.path.altsep (where available) are used.
used to split the pattern into segments and match path separators. If
not given, os.path.sep and os.path.altsep (where available) are used.
"""
if not seps:
if os.path.altsep:
Vendored
+31 -29
View File
@@ -34,16 +34,16 @@ def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_BEST,
encoding=None, errors=None, newline=None):
"""Open a gzip-compressed file in binary or text mode.
The filename argument can be an actual filename (a str or bytes object), or
an existing file object to read from or write to.
The filename argument can be an actual filename (a str or bytes object),
or an existing file object to read from or write to.
The mode argument can be "r", "rb", "w", "wb", "x", "xb", "a" or "ab" for
binary mode, or "rt", "wt", "xt" or "at" for text mode. The default mode is
"rb", and the default compresslevel is 9.
The mode argument can be "r", "rb", "w", "wb", "x", "xb", "a" or "ab"
for binary mode, or "rt", "wt", "xt" or "at" for text mode. The default
mode is "rb", and the default compresslevel is 9.
For binary mode, this function is equivalent to the GzipFile constructor:
GzipFile(filename, mode, compresslevel). In this case, the encoding, errors
and newline arguments must not be provided.
For binary mode, this function is equivalent to the GzipFile
constructor: GzipFile(filename, mode, compresslevel). In this case,
the encoding, errors and newline arguments must not be provided.
For text mode, a GzipFile object is created, and wrapped in an
io.TextIOWrapper instance with the specified encoding, error handling
@@ -148,8 +148,8 @@ class GzipFile(_streams.BaseStream):
"""The GzipFile class simulates most of the methods of a file object with
the exception of the truncate() method.
This class only supports opening files in binary mode. If you need to open a
compressed file in text mode, use the gzip.open() function.
This class only supports opening files in binary mode. If you need to
open a compressed file in text mode, use the gzip.open() function.
"""
@@ -165,31 +165,33 @@ class GzipFile(_streams.BaseStream):
non-trivial value.
The new class instance is based on fileobj, which can be a regular
file, an io.BytesIO object, or any other object which simulates a file.
It defaults to None, in which case filename is opened to provide
a file object.
file, an io.BytesIO object, or any other object which simulates
a file. It defaults to None, in which case filename is opened to
provide a file object.
When fileobj is not None, the filename argument is only used to be
included in the gzip file header, which may include the original
filename of the uncompressed file. It defaults to the filename of
fileobj, if discernible; otherwise, it defaults to the empty string,
and in this case the original filename is not included in the header.
and in this case the original filename is not included in the
header.
The mode argument can be any of 'r', 'rb', 'a', 'ab', 'w', 'wb', 'x', or
'xb' depending on whether the file will be read or written. The default
is the mode of fileobj if discernible; otherwise, the default is 'rb'.
A mode of 'r' is equivalent to one of 'rb', and similarly for 'w' and
'wb', 'a' and 'ab', and 'x' and 'xb'.
The mode argument can be any of 'r', 'rb', 'a', 'ab', 'w', 'wb',
'x', or 'xb' depending on whether the file will be read or written.
The default is the mode of fileobj if discernible; otherwise, the
default is 'rb'. A mode of 'r' is equivalent to one of 'rb', and
similarly for 'w' and 'wb', 'a' and 'ab', and 'x' and 'xb'.
The compresslevel argument is an integer from 0 to 9 controlling the
level of compression; 1 is fastest and produces the least compression,
and 9 is slowest and produces the most compression. 0 is no compression
at all. The default is 9.
The compresslevel argument is an integer from 0 to 9 controlling
the level of compression; 1 is fastest and produces the least
compression, and 9 is slowest and produces the most compression.
0 is no compression at all. The default is 9.
The optional mtime argument is the timestamp requested by gzip. The time
is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970.
If mtime is omitted or None, the current time is used. Use mtime = 0
to generate a compressed stream that does not depend on creation time.
The optional mtime argument is the timestamp requested by gzip.
The time is in Unix format, i.e., seconds since 00:00:00 UTC,
January 1, 1970. If mtime is omitted or None, the current time
is used. Use mtime = 0 to generate a compressed stream that does
not depend on creation time.
"""
@@ -575,10 +577,10 @@ class _GzipReader(_streams.DecompressReader):
# Read a chunk of data from the file
if self._decompressor.needs_input:
buf = self._fp.read(READ_BUFFER_SIZE)
uncompress = self._decompressor.decompress(buf, size)
else:
uncompress = self._decompressor.decompress(b"", size)
buf = b""
uncompress = self._decompressor.decompress(buf, size)
if self._decompressor.unused_data != b"":
# Prepend the already read bytes to the fileobj so they can
# be seen by _read_eof() and _read_gzip_header()
+3 -3
View File
@@ -391,18 +391,18 @@ class Morsel(dict):
return '<%s: %s>' % (self.__class__.__name__, self.OutputString())
def js_output(self, attrs=None):
import base64
import urllib.parse
# 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")
output_encoded = urllib.parse.quote(output_string, safe='', encoding='utf-8')
return """
<script type="text/javascript">
<!-- begin hiding
document.cookie = atob(\"%s\");
document.cookie = decodeURIComponent(\"%s\");
// end hiding -->
</script>
""" % (output_encoded,)
+48
View File
@@ -0,0 +1,48 @@
Guido van Rossum, as well as being the creator of the Python language, is the
original creator of IDLE. Other contributors prior to Version 0.8 include
Mark Hammond, Jeremy Hylton, Tim Peters, and Moshe Zadka.
Until Python 2.3, IDLE's development was carried out in the SF IDLEfork project. The
objective was to develop a version of IDLE which had an execution environment
which could be initialized prior to each run of user code.
IDLefork was merged into the Python code base in 2003.
The IDLEfork project was initiated by David Scherer, with some help from Peter
Schneider-Kamp and Nicholas Riley. David wrote the first version of the RPC
code and designed a fast turn-around environment for VPython. Guido developed
the RPC code and Remote Debugger currently integrated in IDLE. Bruce Sherwood
contributed considerable time testing and suggesting improvements.
Besides David and Guido, the main developers who were active on IDLEfork
are Stephen M. Gava, who implemented the configuration GUI, the new
configuration system, and the About dialog, and Kurt B. Kaiser, who completed
the integration of the RPC and remote debugger, implemented the threaded
subprocess, and made a number of usability enhancements.
Other contributors include Raymond Hettinger, Tony Lownds (Mac integration),
Neal Norwitz (code check and clean-up), Ronald Oussoren (Mac integration),
Noam Raphael (Code Context, Call Tips, many other patches), and Chui Tey (RPC
integration, debugger integration and persistent breakpoints).
Scott David Daniels, Tal Einat, Hernan Foffani, Christos Georgiou,
Jim Jewett, Martin v. Löwis, Jason Orendorff, Guilherme Polo, Josh Robb,
Nigel Rowe, Bruce Sherwood, Jeff Shute, and Weeble have submitted useful
patches. Thanks, guys!
Major contributors since 2005:
- 2005: Tal Einat
- 2010: Terry Jan Reedy (current maintainer)
- 2013: Roger Serwy
- 2014: Saimadhav Heblikar
- 2015: Mark Roseman
- 2017: Louie Lu, Cheryl Sabella, and Serhiy Storchaka
- 2025: Stan Ulbrych
For additional details refer to NEWS.txt and Changelog.
If we missed you, feel free to submit a PR with a summary of
contributions (for instance, at least 5 merged PRs).
+1591
View File
File diff suppressed because it is too large. Load diff
Loaded 100 of 923 files, more files were not shown because too many files have changed in this diff. Show more