core/process: cross-platform PTY via backend and pywinpty

POSIX openpty/fork and Windows ConPTY; win32 marker dep plus typings stub.
This commit is contained in:
2026-07-15 20:01:43 +08:00
parent 611147b0a5
commit c466d09173
9 changed files with 496 additions and 182 deletions
+2 -2
View File
@@ -67,8 +67,8 @@ Module-level `@tool` handlers. Call `set_workspace_root()` before use.
- **`workspace`**: path resolve under root; path substring denylist; command basename denylist; clearer deny messages. - **`workspace`**: path resolve under root; path substring denylist; command basename denylist; clearer deny messages.
- **`file`**: `read_file`, `write_file`, `listdir`, `tree`, `glob_paths`, `grep_files` (regex, skip VCS/binary), `edit_replace`, `edit_lineno` (1-based range), `copy_path` / `move_path` / `delete_path`. - **`file`**: `read_file`, `write_file`, `listdir`, `tree`, `glob_paths`, `grep_files` (regex, skip VCS/binary), `edit_replace`, `edit_lineno` (1-based range), `copy_path` / `move_path` / `delete_path`.
- **`process`**: `run_command` (argv, no shell, timeout, optional stdin/env); PTY `open_pty` / `read_pty` / `write_pty` / `close_pty` (**Unix only**: `pty`+`fork`). - **`process`**: `run_command` (argv, no shell, timeout, optional stdin/env); PTY `open_pty` / `read_pty` / `write_pty` / `close_pty` (POSIX: `pty`+`fork`; Windows: ConPTY via `pywinpty` env marker dep).
- PTY: structured status (`alive`/`exit_code`/`data`); `read_pty(..., until=)`; session limit/idle TTL/output budget; close SIGTERM→SIGKILL. - PTY: backend in `pty_backend.py`; structured status (`alive`/`exit_code`/`data`); `read_pty(..., until=)`; session limit/idle TTL/output budget; close terminate→kill.
- CLI limit hooks: interactive confirm to raise tool-loop rounds, PTY session cap, or PTY output budget. - CLI limit hooks: interactive confirm to raise tool-loop rounds, PTY session cap, or PTY output budget.
- Destructive confirms: `classify_danger` + `ToolRegistry(on_confirm=…)`; CLI default deny; config `confirm_destructive` / `path_denylist`. - Destructive confirms: `classify_danger` + `ToolRegistry(on_confirm=…)`; CLI default deny; config `confirm_destructive` / `path_denylist`.
- **`vcs`**: read-only VCS tools (`vcs_kind` / `vcs_status` / `vcs_diff` / `vcs_log` / `vcs_branch`) via `VcsBackend` protocol; **git** implemented; detectors are pluggable for other systems. - **`vcs`**: read-only VCS tools (`vcs_kind` / `vcs_status` / `vcs_diff` / `vcs_log` / `vcs_branch`) via `VcsBackend` protocol; **git** implemented; detectors are pluggable for other systems.
+1 -1
View File
@@ -138,7 +138,7 @@ User messages are saved immediately. On API error or Ctrl+C, partial assistant/t
## Tools (when enabled) ## Tools (when enabled)
Default registry: file ops, `run_command` / PTY (Unix), read-only VCS (git), and human prompts (`ask_user` / `choose_user` / `form_user`). Default registry: file ops, `run_command` / PTY (POSIX openpty; Windows ConPTY via pywinpty), read-only VCS (git), and human prompts (`ask_user` / `choose_user` / `form_user`).
Safety defaults: Safety defaults:
Generated
+109 -100
View File
@@ -5,7 +5,7 @@
groups = ["default", "dev"] groups = ["default", "dev"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:cd5131c309485626148d1933a2a557a9cb6a41038b980bc26a453def4c38032c" content_hash = "sha256:f244597f4e94cc6f75cb0e08bf337a7f09e7cc4e9444726be86e488247350a49"
[[metadata.targets]] [[metadata.targets]]
requires_python = "~=3.14" requires_python = "~=3.14"
@@ -52,45 +52,39 @@ files = [
[[package]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.7" version = "3.4.9"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
groups = ["default"] groups = ["default"]
files = [ files = [
{file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"},
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"},
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"},
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"},
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"},
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"},
{file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"},
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"},
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"},
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"},
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"},
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"},
{file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"},
{file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"},
{file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"},
{file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"},
{file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"},
{file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"},
{file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"},
] ]
[[package]] [[package]]
@@ -259,7 +253,7 @@ files = [
[[package]] [[package]]
name = "niquests" name = "niquests"
version = "3.20.0" version = "3.20.1"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "Niquests is a simple, yet elegant, HTTP library. It is a drop-in replacement for Requests, which is under feature freeze." summary = "Niquests is a simple, yet elegant, HTTP library. It is a drop-in replacement for Requests, which is under feature freeze."
groups = ["default"] groups = ["default"]
@@ -269,8 +263,8 @@ dependencies = [
"wassima<3,>=1.0.1; sys_platform != \"emscripten\"", "wassima<3,>=1.0.1; sys_platform != \"emscripten\"",
] ]
files = [ files = [
{file = "niquests-3.20.0-py3-none-any.whl", hash = "sha256:86281a62314d7395f049d748480589e273dc1f429953afbac377fff142860429"}, {file = "niquests-3.20.1-py3-none-any.whl", hash = "sha256:71062880739c94d67aead1f8b7972baad3236b97a55a490493729789d1639fc4"},
{file = "niquests-3.20.0.tar.gz", hash = "sha256:dba85ce23ac5052f0a1e1c1cf1c017511a993f906686e8b6c84e0b54818fd1dc"}, {file = "niquests-3.20.1.tar.gz", hash = "sha256:55951812cf997963bfef8e4f797ff0460ca571e53ec184039b010e12152c0e41"},
] ]
[[package]] [[package]]
@@ -374,76 +368,91 @@ files = [
{file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"},
] ]
[[package]]
name = "pywinpty"
version = "3.0.5"
requires_python = ">=3.10"
summary = "Pseudo terminal support for Windows from Python."
groups = ["default"]
marker = "sys_platform == \"win32\""
files = [
{file = "pywinpty-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:03bb3c16d691d9242267201830bcd0e64a9b663170e9042bc84b210da9de15ac"},
{file = "pywinpty-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:89c5c6ef08997a3b4b277b214a35fe15cab4dd6d119f0140aa71df5b1168fdbc"},
{file = "pywinpty-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7b566165e0c5fdd6abe167a5ac8b954be6a843eb55a85946576d6bc1dea03d6d"},
{file = "pywinpty-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:24366280a8aa677323da87bec729cb3ea3b35367386cece0978bdc6e4695c690"},
{file = "pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b"},
]
[[package]] [[package]]
name = "qh3" name = "qh3"
version = "1.9.2" version = "1.9.4"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "A lightway and fast implementation of QUIC and HTTP/3" summary = "A lightway and fast implementation of QUIC and HTTP/3"
groups = ["default"] groups = ["default"]
marker = "(platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and platform_python_implementation == \"PyPy\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and python_version < \"3.12\" or (platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and python_full_version > \"3.7.10\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and platform_python_implementation == \"CPython\"" marker = "(platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and platform_python_implementation == \"PyPy\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and python_version < \"3.12\" or (platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and python_full_version > \"3.7.10\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and platform_python_implementation == \"CPython\""
files = [ files = [
{file = "qh3-1.9.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e38d88f41c6855184a53a1e583d56ae5163b8b70c7e2dc273889ec303cc27585"}, {file = "qh3-1.9.4-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:100368855b0e103774438aaf8b38c711fb51b79f18e3b9dc873ea573648bcc3d"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3beebd98b08bd2fe951ae83b6e52dee3821158ab21ba05f15fbdb401bd6e19f9"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9d45e544e0a556380bd2a92af180ee98d32bca6cf622f28350ebc000611bdf"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6afe20084235ff3c20256c952059277b5a0f5144d63d1892d1dfb5d1b635faef"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43f80d662078c26563a1aac32a4df22649473d996bf2422e073c8aa55b1c837c"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c8f2fdf124acef06306ee50421ee80d37dbc7dfd41ddcec2bb63f3e2508bd74"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:551f60d487d849d6f2313dda94e54ebb16b1a29acb93676540ae790a56308c24"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:adcb00af8c4fddbb266ae4155ec5932dcb0a27dc14d7f7a27fad6a6dce9efd5e"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3755d777ff35561bc719f571ebc4b91aaa8a945c210e50b2576bf2cc69cb511f"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcbf94d8ab4223873f2fc2f7a0992ee0dd557f0ab5bce1ca12314e60a1d5cd39"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04b64bb18e10d8f482bea1ef43e5da3448e2d5ba78bb0c735da5b9e02d7dfb0e"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13b458a440f91d5336dd65f4a624b7eeac0ce1b2a8d02e250df32470606c6775"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b47597daa4ae9195f81a35822217a395d3277f01a393d9c2bb3796a6b458b22c"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7b0429d9c5d892633ee822a428c840201a2ef53a16cec4d4aeb1ae08146cae9"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9bb9012bc02c44da6af1b931b6082588519685fad8a1f866c341f749394f53f"},
{file = "qh3-1.9.2-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:d4ab482b9f12abb5fc6fc67a214431037d094b38673efd6d0df5d64338dd5351"}, {file = "qh3-1.9.4-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9c7f3781ec022dd958b5eb0e5cb2e2a54e207a3d8837fb7290bc239912648fd7"},
{file = "qh3-1.9.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:ab254e2289bfc949b0177aa7739ac963f54355716aee39966120ca71d7315aeb"}, {file = "qh3-1.9.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:633a24ab2db76d64881876c9d0f1b226a47d892bd7d78e61ea2c788ab59570af"},
{file = "qh3-1.9.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:9a6aa7146a62c88fbc8adfa6dcae435670a8487e5f3bb00f92a29060cd630102"}, {file = "qh3-1.9.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:f2ff36b84a4ab9b68806af1f6985fa3b47e29695effe766a3e70c21efac8157c"},
{file = "qh3-1.9.2-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:2710e359d9320014a33149853cbc8c4bd9ca83181bb7a896c7704a1e429c27c4"}, {file = "qh3-1.9.4-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:2100697d736dbdfd1d3f0266c9fcadea6230b66ed59e3e7f8c9a9d1a6eec0c65"},
{file = "qh3-1.9.2-cp314-cp314t-musllinux_1_1_riscv64.whl", hash = "sha256:4d136fa78171ffc094e06c25361fe00482accdb406a442ce93e8121314880659"}, {file = "qh3-1.9.4-cp314-cp314t-musllinux_1_1_riscv64.whl", hash = "sha256:9ce179efaf97f927f01fe158619f5624d8d874c7161b7c497f06897cdde210d5"},
{file = "qh3-1.9.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a48cc6b4d21ef85071ef2f20ba27d7412827ff8385baa19d14b0c58e44fdbc57"}, {file = "qh3-1.9.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:476ee4fcdbd3454eb0eb4dee5678294f06c08ca8e8b83160e2d0ae3c4ae34603"},
{file = "qh3-1.9.2-cp314-cp314t-win32.whl", hash = "sha256:c4dc40c1ac0f31bfc0dadeaf02578460c7456498f549bb6ae742883f90286243"}, {file = "qh3-1.9.4-cp314-cp314t-win32.whl", hash = "sha256:fe9df8136bf358d45a2757287523abaeb208fcbe2c1e34664562eb2d8f64dbdb"},
{file = "qh3-1.9.2-cp314-cp314t-win_amd64.whl", hash = "sha256:19893cfe25a749c363e81415dcdb3b027707e71a09b0e1d4f1e3098d7e5975d3"}, {file = "qh3-1.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:1fbdf53feb1732faf6c1cb34f2e1cbe4b543054db7b4d19d8752f1162e9370be"},
{file = "qh3-1.9.2-cp314-cp314t-win_arm64.whl", hash = "sha256:75b587b26a8cdeb1c715e5d2ec08d4987be8e02bb76a6b24a4075293ade0bd3d"}, {file = "qh3-1.9.4-cp314-cp314t-win_arm64.whl", hash = "sha256:e03ee1e2157a31c69415c3dbfbfe64e02469499ca46e968d13d8a7b289bf520a"},
{file = "qh3-1.9.2-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aabb7c86f49c668ab5bfb297c5da18755a3ca9cf362f8ce73b86f55f43f662fc"}, {file = "qh3-1.9.4-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:26f7c8ed1a7ac0e25b654f030a4f472830c80890cc284dc40a31c17ecc21fbf0"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e632d420d88e30e4e3fc9f5457c362e964e93e91f9c90d148b68b0d4b0dc06"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57b3dfe5dd2da2084adea63dba0bf587a4096ebf9207339187c90139d1f460d"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5260ff1c42f7221fb7e92bc9c55a009341c2851f2dbb28475ddcf0a32c2226a"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7fa1a3debb5199da3c7f45b0ca6efa55174dfd6229370e774099073143ebe9e9"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:304e81b4cf3f97610a1aceaa1aec90a97f0c700071efcc9d6a3b86da0b57539d"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dc9ff40d3a06f27b47d93417666f03ba6fd6b9585d9711ebbddf84fa4b7c50f"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:aa20581d3daf8f80b62f42a10c1f69c12f3b6310f141a2e2f4259de588e570fe"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:010b132c33719af3df4c1d6f569e52a2a20046c55a31c5532e741019c8c53868"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd3a3d7d737644a4d24117613e89220cd5223cb1e0c8281a5a65a97ebaf1efea"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:124045b0c04c05afc038a239d7717019b4c4b9d0cb967040b4331f4d588eff7d"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82edac5bc2eea0881baa142d93e80469347875909acc19ce5e4236b50c465f8e"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a94a1726bf46a451a0295d68fafde28794e83ab6fda7457cbd9309c38151de9"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b18b9d2f892bb95f7f3d3428bb4166f974c9a2e26fcf91b33dda9b4285e7928"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b361e627b6a529ecb231c1d5f3f2abd6d1952b0d7cc02c7718d9657d55dae37"},
{file = "qh3-1.9.2-cp37-abi3-manylinux_2_39_riscv64.whl", hash = "sha256:06f2a10a01b71137fef8bb7b4115f1016d6e4a7f056ce41517ff157e808bf444"}, {file = "qh3-1.9.4-cp37-abi3-manylinux_2_39_riscv64.whl", hash = "sha256:43b6cfb2240836e3e54bbe61ad1b8fa5716516ff5359131a2421c55676a24f26"},
{file = "qh3-1.9.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e15519dc326315c52ab708028bf72aa9162eb3c1bd0ba130dc6d371b000639d2"}, {file = "qh3-1.9.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e6db3def610e9c2d2f3602d3e9c8b2156273225f6eedf74a4749d3bc21af8167"},
{file = "qh3-1.9.2-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:7e04bf8bbbfb8a9f363fd38834aa9c52fcc0164182c72ea10cf30a3e2df066c9"}, {file = "qh3-1.9.4-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:22d5879dc93d2c7ee14cc5933b12c7780d9c7705dff4553becea152fcb767d99"},
{file = "qh3-1.9.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:e3b0b0d17268d57b808a658db7fd7c5e62561ad6d462949d8466c92b22551067"}, {file = "qh3-1.9.4-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:2c2b2005fe6c605d61cac05ec2b3c019cc546184fa43444f3d1e3d22ed020ab2"},
{file = "qh3-1.9.2-cp37-abi3-musllinux_1_1_riscv64.whl", hash = "sha256:efe5ad0a9df71a9a5881615b0c0888767ad3a7cab05e54ba98db52d89d0b796a"}, {file = "qh3-1.9.4-cp37-abi3-musllinux_1_1_riscv64.whl", hash = "sha256:e991b0f20b172fea466d0ffd499a85bf2f26decdeeda537275c72b5f7b26ccfe"},
{file = "qh3-1.9.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:742dd7c5e2207db27d24036ca599cad056bd5e58a5ce1b43f0c424df149e17d3"}, {file = "qh3-1.9.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c13775ed27a88b297f6a1aa05ffb286879453f99e1e53e0af2b22e7ed8f92ebf"},
{file = "qh3-1.9.2-cp37-abi3-win32.whl", hash = "sha256:8d1f185a76578929b0de3dcef135c0cc21d4bff0ef00bd75127d244ba3426125"}, {file = "qh3-1.9.4-cp37-abi3-win32.whl", hash = "sha256:952abe1be90c0114993886792a8a5ec2211f8391df41227f5b7a5a02ff71bc26"},
{file = "qh3-1.9.2-cp37-abi3-win_amd64.whl", hash = "sha256:643e8aa05519dddaf20159d1c13c15473de7649622a1c438968ce0f0da973951"}, {file = "qh3-1.9.4-cp37-abi3-win_amd64.whl", hash = "sha256:5543cb40a48d401f3fa4ec89398e3a213634f195716a9d06d70417c05edbbb32"},
{file = "qh3-1.9.2-cp37-abi3-win_arm64.whl", hash = "sha256:87bace13780b3c2238db1dd31deb10152203d5b360ca6515c78b853796245606"}, {file = "qh3-1.9.4-cp37-abi3-win_arm64.whl", hash = "sha256:d0bd73b3e73f4f80d2430d410b532222ecf57e9d4fe8600b7683c775cbff9479"},
{file = "qh3-1.9.2.tar.gz", hash = "sha256:c6c92f63c2ec292256b5a5ed9345c42344bdaca2e55ec795623987a563aea19c"}, {file = "qh3-1.9.4.tar.gz", hash = "sha256:bd2ea9baf19656c544a48a56a195f2ac257cd973b566f5f2998fa3b7446281a1"},
] ]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.20" version = "0.15.21"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust." summary = "An extremely fast Python linter and code formatter, written in Rust."
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078"}, {file = "ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d"},
{file = "ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b"}, {file = "ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd"},
{file = "ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632"}, {file = "ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646"},
{file = "ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd"}, {file = "ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be"},
{file = "ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b"}, {file = "ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd"},
{file = "ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267"}, {file = "ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41"},
{file = "ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c"}, {file = "ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86"},
{file = "ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae"}, {file = "ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67"},
{file = "ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b"}, {file = "ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8"},
{file = "ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487"}, {file = "ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075"},
{file = "ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3"}, {file = "ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341"},
{file = "ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053"}, {file = "ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d"},
{file = "ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4"}, {file = "ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233"},
{file = "ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460"}, {file = "ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f"},
{file = "ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21"}, {file = "ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd"},
{file = "ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415"}, {file = "ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3"},
{file = "ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca"}, {file = "ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8"},
{file = "ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566"}, {file = "ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500"},
] ]
[[package]] [[package]]
@@ -500,7 +509,7 @@ files = [
[[package]] [[package]]
name = "urllib3-future" name = "urllib3-future"
version = "2.22.900" version = "2.22.901"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "urllib3.future is a powerful HTTP 1.1, 2, and 3 client with both sync and async interfaces" summary = "urllib3.future is a powerful HTTP 1.1, 2, and 3 client with both sync and async interfaces"
groups = ["default"] groups = ["default"]
@@ -510,18 +519,18 @@ dependencies = [
"qh3<2.0.0,>=1.5.4; (platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and platform_python_implementation == \"PyPy\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and python_version < \"3.12\" or (platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and python_full_version > \"3.7.10\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and platform_python_implementation == \"CPython\"", "qh3<2.0.0,>=1.5.4; (platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and platform_python_implementation == \"PyPy\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and python_version < \"3.12\" or (platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and python_full_version > \"3.7.10\" and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"aarch64\" or platform_machine == \"arm64\" or platform_machine == \"ARM64\" or platform_machine == \"x86\" or platform_machine == \"i686\" or platform_machine == \"riscv64\" or platform_machine == \"riscv64gc\") and platform_python_implementation == \"CPython\"",
] ]
files = [ files = [
{file = "urllib3_future-2.22.900-py3-none-any.whl", hash = "sha256:803976fe865f03d61d5ddc17dbdbac2bacc08202aad303f282cc4af319ddbfd2"}, {file = "urllib3_future-2.22.901-py3-none-any.whl", hash = "sha256:dc2d4572ba612be69e3f6db87d1f469b7d08f107e80fd8749081d7e6a5a2fe10"},
{file = "urllib3_future-2.22.900.tar.gz", hash = "sha256:3161900c00cf41db90dd3d938c1d14960faab475dd6ca0bf5ecd06112327c714"}, {file = "urllib3_future-2.22.901.tar.gz", hash = "sha256:fbece0ff51299a213c926897ffbed61d820ab5ef2ba81cece361c049fe2f51a9"},
] ]
[[package]] [[package]]
name = "wassima" name = "wassima"
version = "2.1.1" version = "2.1.2"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "Access your OS root certificates with utmost ease" summary = "Access your OS root certificates with utmost ease"
groups = ["default"] groups = ["default"]
marker = "sys_platform != \"emscripten\"" marker = "sys_platform != \"emscripten\""
files = [ files = [
{file = "wassima-2.1.1-py3-none-any.whl", hash = "sha256:ab0f12c091ff697f111eea5f925aafd83736909a1c9c764a8bbf874b2e4d4a42"}, {file = "wassima-2.1.2-py3-none-any.whl", hash = "sha256:9a5779e8ade39960973c6cc17e095cc998d6345ab3dbdd1ec2d7fde8c356fb82"},
{file = "wassima-2.1.1.tar.gz", hash = "sha256:9c6ad4aa3cfbe91fd75f9eae315ba563bbc7d9d2479aef0c288fa7f1ca3b0c53"}, {file = "wassima-2.1.2.tar.gz", hash = "sha256:f74b5441151728c54118ece6d747cfe92b2c595a3d062a1955f42a2bc894cd10"},
] ]
+12 -1
View File
@@ -14,7 +14,8 @@ dependencies = [
"sqlalchemy>=2.0.51", "sqlalchemy>=2.0.51",
"aiosqlite>=0.22.1", "aiosqlite>=0.22.1",
"click>=8.4.2", "click>=8.4.2",
"awaitlet>=0.0.1" "awaitlet>=0.0.1",
"pywinpty>=2.0.0; sys_platform == \"win32\""
] ]
requires-python = ">=3.14,<4" requires-python = ">=3.14,<4"
readme = "README.md" readme = "README.md"
@@ -38,6 +39,8 @@ build-backend = "pdm.backend"
[tool.basedpyright] [tool.basedpyright]
typeCheckingMode = "recommended" typeCheckingMode = "recommended"
strictGenericNarrowing = true strictGenericNarrowing = true
# winpty is Windows-only (pywinpty); stubs live under typings/ (pyright default stubPath).
reportMissingModuleSource = "hint"
reportAny = "hint" reportAny = "hint"
reportExplicitAny = "hint" reportExplicitAny = "hint"
reportImplicitStringConcatenation = "hint" reportImplicitStringConcatenation = "hint"
@@ -60,6 +63,10 @@ reportUnusedParameter = "none"
reportMissingImports = "none" reportMissingImports = "none"
reportMissingTypeStubs = "none" reportMissingTypeStubs = "none"
[[tool.basedpyright.executionEnvironments]]
root = "scripts"
reportUnusedCallResult = "none"
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_mode = "auto" asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function" asyncio_default_fixture_loop_scope = "function"
@@ -163,3 +170,7 @@ allowed-confusables = [
"TRY003", # long exception messages in tests "TRY003", # long exception messages in tests
"FBT", # boolean positional args in helpers "FBT", # boolean positional args in helpers
] ]
"scripts/**" = [
"T201", # CLI scripts print status
"PLR0911", # command dispatch returns
]
+4 -3
View File
@@ -10,10 +10,11 @@ from .pty_session import PtyManager
@tool @tool
def open_pty(command: list[str], *, cwd: str = ".") -> str: def open_pty(command: list[str], *, cwd: str = ".") -> str:
"""Open a Unix PTY session running ``command`` (argv) under the workspace. """Open a PTY session running ``command`` (argv) under the workspace.
Returns structured status including session_id. Not supported on Windows. POSIX uses openpty/fork; Windows uses ConPTY (pywinpty). Returns structured
Failed exec surfaces via later read_pty data (marker) and exit_code=127. status including session_id. Failed exec may surface via later read_pty
data (marker) and a non-zero exit_code.
""" """
try: try:
session = PtyManager.open(command, cwd=cwd) session = PtyManager.open(command, cwd=cwd)
+287
View File
@@ -0,0 +1,287 @@
"""Platform PTY backends (POSIX openpty/fork; Windows ConPTY via pywinpty)."""
from __future__ import annotations
import contextlib
import os
import select
import signal
import sys
import time
from dataclasses import dataclass
from typing import TYPE_CHECKING, Protocol, runtime_checkable
if TYPE_CHECKING:
from pathlib import Path
@runtime_checkable
class PtyHandle(Protocol):
"""One live PTY-backed child process."""
@property
def master_fd(self) -> int | None:
"""POSIX master FD when applicable; ``None`` on Windows ConPTY."""
...
@property
def pid(self) -> int | None:
"""POSIX child pid when applicable; ``None`` on Windows ConPTY."""
...
def poll_exit(self) -> tuple[bool, int | None]:
"""Return ``(alive, exit_code)`` without blocking."""
...
def read_bytes(self, max_bytes: int, timeout: float) -> bytes:
"""Read up to *max_bytes* waiting up to *timeout* seconds."""
...
def write_bytes(self, data: bytes) -> None:
"""Write raw bytes to the PTY input."""
...
def terminate(self) -> None:
"""Request graceful shutdown (SIGTERM / TerminateProcess soft)."""
...
def kill(self) -> None:
"""Force-kill the child."""
...
def wait_reap(self, *, timeout: float | None = None) -> None:
"""Wait for the child to exit and reap it."""
...
def close_resources(self) -> None:
"""Release OS handles/FDs (after terminate/kill)."""
...
_EXEC_FAIL_MARKER = b"plyngent-pty-exec-failed: "
_STDERR_FD = 2
@dataclass
class PosixPtyHandle:
master_fd: int
pid: int
_alive: bool = True
_exit_code: int | None = None
_closed: bool = False
def poll_exit(self) -> tuple[bool, int | None]:
if not self._alive or self._closed:
return self._alive, self._exit_code
try:
waited_pid, status = os.waitpid(self.pid, os.WNOHANG)
except ChildProcessError:
self._alive = False
return False, self._exit_code
if waited_pid == 0:
return True, None
self._alive = False
if os.WIFEXITED(status):
self._exit_code = os.WEXITSTATUS(status)
elif os.WIFSIGNALED(status):
self._exit_code = -os.WTERMSIG(status)
else:
self._exit_code = status
return False, self._exit_code
def read_bytes(self, max_bytes: int, timeout: float) -> bytes:
if self._closed or max_bytes < 1:
return b""
data = b""
try:
ready, _, _ = select.select([self.master_fd], [], [], timeout)
if ready:
data = os.read(self.master_fd, max_bytes)
except (OSError, ValueError):
data = b""
_ = self.poll_exit()
return data
def write_bytes(self, data: bytes) -> None:
_ = os.write(self.master_fd, data)
def terminate(self) -> None:
if not self._alive:
return
with contextlib.suppress(ProcessLookupError):
os.kill(self.pid, signal.SIGTERM)
def kill(self) -> None:
if not self._alive:
return
with contextlib.suppress(ProcessLookupError):
os.kill(self.pid, signal.SIGKILL)
with contextlib.suppress(ChildProcessError):
_ = os.waitpid(self.pid, 0)
self._alive = False
if self._exit_code is None:
self._exit_code = -signal.SIGKILL
def wait_reap(self, *, timeout: float | None = None) -> None:
deadline = None if timeout is None else time.monotonic() + max(0.0, timeout)
while self._alive:
_ = self.poll_exit()
if not self._alive:
with contextlib.suppress(ChildProcessError):
_ = os.waitpid(self.pid, os.WNOHANG)
return
if deadline is not None and time.monotonic() >= deadline:
return
time.sleep(0.05)
def close_resources(self) -> None:
if self._closed:
return
with contextlib.suppress(OSError):
os.close(self.master_fd)
self._closed = True
if sys.platform == "win32":
from winpty import PtyProcess
@dataclass
class WinPtyHandle:
"""ConPTY session via pywinpty ``PtyProcess``."""
process: PtyProcess
_alive: bool = True
_exit_code: int | None = None
_closed: bool = False
@property
def master_fd(self) -> int | None:
return None
@property
def pid(self) -> int | None:
return None
def poll_exit(self) -> tuple[bool, int | None]:
if not self._alive or self._closed:
return self._alive, self._exit_code
try:
alive = bool(self.process.isalive())
except Exception: # noqa: BLE001 — treat as dead on probe failure
alive = False
if alive:
return True, None
self._alive = False
status = self.process.exitstatus
if isinstance(status, int):
self._exit_code = status
return False, self._exit_code
def read_bytes(self, max_bytes: int, timeout: float) -> bytes:
"""Read up to *max_bytes*.
pywinpty ``PtyProcess.read(size)`` is blocking and has no timeout kwarg;
*timeout* is accepted for API parity with the POSIX backend.
"""
del timeout # ConPTY read has no timed wait in pywinpty 3.x
if self._closed or max_bytes < 1:
return b""
try:
raw = self.process.read(max_bytes)
except Exception: # noqa: BLE001 — EOF / closed PTY
_ = self.poll_exit()
return b""
data = raw.encode("utf-8", errors="replace")
if len(data) > max_bytes:
data = data[:max_bytes]
_ = self.poll_exit()
return data
def write_bytes(self, data: bytes) -> None:
text = data.decode("utf-8", errors="replace")
_ = self.process.write(text)
def terminate(self) -> None:
if not self._alive:
return
with contextlib.suppress(Exception):
_ = self.process.terminate(force=False)
def kill(self) -> None:
with contextlib.suppress(Exception):
_ = self.process.terminate(force=True)
with contextlib.suppress(Exception):
self.process.kill()
self._alive = False
if self._exit_code is None:
self._exit_code = 1
def wait_reap(self, *, timeout: float | None = None) -> None:
deadline = None if timeout is None else time.monotonic() + max(0.0, timeout)
while self._alive:
_ = self.poll_exit()
if not self._alive:
return
if deadline is not None and time.monotonic() >= deadline:
return
time.sleep(0.05)
def close_resources(self) -> None:
if self._closed:
return
with contextlib.suppress(Exception):
self.process.close(force=True)
self._closed = True
def pty_available() -> bool:
return True
def spawn_pty(command: list[str], *, cwd: Path) -> PtyHandle:
if not command:
msg = "command argv must not be empty"
raise OSError(msg)
try:
process = PtyProcess.spawn(command, cwd=str(cwd))
except Exception as exc:
msg = f"failed to open ConPTY: {exc}"
raise OSError(msg) from exc
return WinPtyHandle(process=process)
else:
def pty_available() -> bool:
return hasattr(os, "fork")
def spawn_pty(command: list[str], *, cwd: Path) -> PtyHandle:
if not command:
msg = "command argv must not be empty"
raise OSError(msg)
return _spawn_posix(command, cwd=cwd)
def _spawn_posix(command: list[str], *, cwd: Path) -> PosixPtyHandle:
import pty
master_fd, slave_fd = pty.openpty()
with contextlib.suppress(OSError, AttributeError):
os.set_inheritable(master_fd, False) # noqa: FBT003 — stdlib API
pid = os.fork()
if pid == 0: # child — fork-then-exec only
try:
os.close(master_fd)
_ = os.setsid()
_ = os.dup2(slave_fd, 0)
_ = os.dup2(slave_fd, 1)
_ = os.dup2(slave_fd, _STDERR_FD)
if slave_fd > _STDERR_FD:
os.close(slave_fd)
_ = os.chdir(cwd)
os.execvp(command[0], command)
except OSError as exc:
with contextlib.suppress(OSError):
_ = os.write(1, _EXEC_FAIL_MARKER + str(exc).encode(errors="replace") + b"\n")
os._exit(127)
os.close(slave_fd)
with contextlib.suppress(OSError, AttributeError):
os.set_inheritable(master_fd, False) # noqa: FBT003 — stdlib API
return PosixPtyHandle(master_fd=master_fd, pid=pid)
+38 -75
View File
@@ -1,17 +1,13 @@
"""Unix PTY session registry (requires ``pty`` + ``os.fork``; not supported on Windows).""" """Cross-platform PTY session registry (POSIX openpty/fork; Windows ConPTY)."""
from __future__ import annotations from __future__ import annotations
import contextlib
import os
import pty
import select
import signal
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from threading import Lock from threading import Lock
from typing import TYPE_CHECKING, ClassVar from typing import TYPE_CHECKING, ClassVar
from plyngent.tools.process.pty_backend import PtyHandle, pty_available, spawn_pty
from plyngent.tools.workspace import WorkspaceError, check_command_allowed, resolve_path from plyngent.tools.workspace import WorkspaceError, check_command_allowed, resolve_path
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -27,15 +23,12 @@ DEFAULT_SESSION_OUTPUT_BUDGET = 256_000
DEFAULT_CLOSE_GRACE_SECONDS = 0.5 DEFAULT_CLOSE_GRACE_SECONDS = 0.5
_SESSION_LIMIT_STEP = 4 _SESSION_LIMIT_STEP = 4
_BUDGET_STEP = 256_000 _BUDGET_STEP = 256_000
_STDERR_FD = 2
_EXEC_FAIL_MARKER = b"plyngent-pty-exec-failed: "
@dataclass @dataclass
class PtySession: class PtySession:
session_id: int session_id: int
master_fd: int handle: PtyHandle
pid: int
closed: bool = False closed: bool = False
alive: bool = True alive: bool = True
exit_code: int | None = None exit_code: int | None = None
@@ -45,6 +38,16 @@ class PtySession:
output_budget: int = DEFAULT_SESSION_OUTPUT_BUDGET output_budget: int = DEFAULT_SESSION_OUTPUT_BUDGET
command: tuple[str, ...] = () command: tuple[str, ...] = ()
@property
def master_fd(self) -> int | None:
"""POSIX master FD when available (``None`` on Windows ConPTY)."""
return self.handle.master_fd
@property
def pid(self) -> int | None:
"""POSIX child pid when available (``None`` on Windows ConPTY)."""
return self.handle.pid
@dataclass(frozen=True) @dataclass(frozen=True)
class PtyReadResult: class PtyReadResult:
@@ -114,6 +117,9 @@ class PtyManager:
*, *,
cwd: str = ".", cwd: str = ".",
) -> PtySession: ) -> PtySession:
if not pty_available():
msg = "PTY is not available on this platform (Windows needs pywinpty)"
raise WorkspaceError(msg)
check_command_allowed(command) check_command_allowed(command)
workdir = resolve_path(cwd) workdir = resolve_path(cwd)
if not workdir.is_dir(): if not workdir.is_dir():
@@ -131,37 +137,18 @@ class PtyManager:
msg = f"{reason}; close idle sessions or allow a higher limit" msg = f"{reason}; close idle sessions or allow a higher limit"
raise WorkspaceError(msg) raise WorkspaceError(msg)
master_fd, slave_fd = pty.openpty() try:
# Parent must not pass master into the child (or future children). handle = spawn_pty(command, cwd=workdir)
with contextlib.suppress(OSError, AttributeError): except OSError as exc:
os.set_inheritable(master_fd, False) # noqa: FBT003 — stdlib API msg = f"failed to open PTY: {exc}"
pid = os.fork() raise WorkspaceError(msg) from exc
if pid == 0: # child — keep path fork-then-exec only (no Python after exec fail except _exit)
try:
os.close(master_fd)
_ = os.setsid()
_ = os.dup2(slave_fd, 0)
_ = os.dup2(slave_fd, 1)
_ = os.dup2(slave_fd, _STDERR_FD)
if slave_fd > _STDERR_FD:
os.close(slave_fd)
_ = os.chdir(workdir)
os.execvp(command[0], command)
except OSError as exc:
with contextlib.suppress(OSError):
_ = os.write(1, _EXEC_FAIL_MARKER + str(exc).encode(errors="replace") + b"\n")
os._exit(127)
os.close(slave_fd)
with contextlib.suppress(OSError, AttributeError):
os.set_inheritable(master_fd, False) # noqa: FBT003 — stdlib API
with cls._lock: with cls._lock:
session_id = cls._next_id session_id = cls._next_id
cls._next_id += 1 cls._next_id += 1
session = PtySession( session = PtySession(
session_id=session_id, session_id=session_id,
master_fd=master_fd, handle=handle,
pid=pid,
command=tuple(command), command=tuple(command),
output_budget=cls.session_output_budget, output_budget=cls.session_output_budget,
) )
@@ -181,20 +168,10 @@ class PtyManager:
def _poll_exit(cls, session: PtySession) -> None: def _poll_exit(cls, session: PtySession) -> None:
if not session.alive or session.closed: if not session.alive or session.closed:
return return
try: alive, exit_code = session.handle.poll_exit()
waited_pid, status = os.waitpid(session.pid, os.WNOHANG) session.alive = alive
except ChildProcessError: if exit_code is not None:
session.alive = False session.exit_code = exit_code
return
if waited_pid == 0:
return
session.alive = False
if os.WIFEXITED(status):
session.exit_code = os.WEXITSTATUS(status)
elif os.WIFSIGNALED(status):
session.exit_code = -os.WTERMSIG(status)
else:
session.exit_code = status
@classmethod @classmethod
def refresh(cls, session_id: int) -> PtySession: def refresh(cls, session_id: int) -> PtySession:
@@ -210,13 +187,7 @@ class PtyManager:
if session.closed or (session.output_budget - session.bytes_read) <= 0: if session.closed or (session.output_budget - session.bytes_read) <= 0:
return b"" return b""
to_read = min(max_bytes, session.output_budget - session.bytes_read) to_read = min(max_bytes, session.output_budget - session.bytes_read)
data = b"" data = session.handle.read_bytes(to_read, timeout)
try:
ready, _, _ = select.select([session.master_fd], [], [], timeout)
if ready:
data = os.read(session.master_fd, to_read)
except OSError, ValueError:
data = b""
cls._poll_exit(session) cls._poll_exit(session)
if not data: if not data:
return b"" return b""
@@ -336,7 +307,7 @@ class PtyManager:
msg = f"PTY session process is not alive: {session_id}" msg = f"PTY session process is not alive: {session_id}"
raise WorkspaceError(msg) raise WorkspaceError(msg)
try: try:
_ = os.write(session.master_fd, data.encode()) session.handle.write_bytes(data.encode())
except OSError as exc: except OSError as exc:
cls._poll_exit(session) cls._poll_exit(session)
msg = f"failed to write PTY: {exc}" msg = f"failed to write PTY: {exc}"
@@ -366,27 +337,19 @@ class PtyManager:
cls._poll_exit(session) cls._poll_exit(session)
if session.alive: if session.alive:
with contextlib.suppress(ProcessLookupError): session.handle.terminate()
os.kill(session.pid, signal.SIGTERM) session.handle.wait_reap(timeout=max(0.0, grace_seconds))
deadline = time.monotonic() + max(0.0, grace_seconds) cls._poll_exit(session)
while session.alive and time.monotonic() < deadline:
time.sleep(0.05)
cls._poll_exit(session)
if session.alive: if session.alive:
with contextlib.suppress(ProcessLookupError): session.handle.kill()
os.kill(session.pid, signal.SIGKILL) session.handle.wait_reap(timeout=1.0)
with contextlib.suppress(ChildProcessError): cls._poll_exit(session)
_ = os.waitpid(session.pid, 0) if session.alive:
session.alive = False session.alive = False
if session.exit_code is None: if session.exit_code is None:
session.exit_code = -signal.SIGKILL session.exit_code = session.handle.poll_exit()[1]
else:
# Ensure reaped
with contextlib.suppress(ChildProcessError):
_ = os.waitpid(session.pid, os.WNOHANG)
with contextlib.suppress(OSError): session.handle.close_resources()
os.close(session.master_fd)
session.closed = True session.closed = True
with cls._lock: with cls._lock:
_ = cls._sessions.pop(session_id, None) _ = cls._sessions.pop(session_id, None)
+13
View File
@@ -246,12 +246,25 @@ async def test_pty_output_budget_is_per_session(workspace: object) -> None:
def test_pty_master_not_inheritable(workspace: object) -> None: def test_pty_master_not_inheritable(workspace: object) -> None:
del workspace del workspace
import os import os
import sys
if sys.platform == "win32":
import pytest
pytest.skip("master FD inheritance is POSIX-only")
try: try:
opened = call_sync(open_pty, ["sleep", "5"]) opened = call_sync(open_pty, ["sleep", "5"])
session_id = _session_id(opened) session_id = _session_id(opened)
session = PtyManager.get(session_id) session = PtyManager.get(session_id)
assert session is not None assert session is not None
assert session.master_fd is not None
assert os.get_inheritable(session.master_fd) is False assert os.get_inheritable(session.master_fd) is False
finally: finally:
PtyManager.close_all() PtyManager.close_all()
def test_pty_backend_available() -> None:
from plyngent.tools.process.pty_backend import pty_available
assert pty_available() is True
+30
View File
@@ -0,0 +1,30 @@
"""Stubs for pywinpty (ConPTY) matching winpty.PtyProcess runtime API."""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
class PtyProcess:
exitstatus: int | None
@classmethod
def spawn(
cls,
argv: str | Sequence[str],
cwd: str | None = None,
env: dict[str, str] | None = None,
dimensions: tuple[int, int] = (24, 80),
backend: int | None = None,
) -> PtyProcess: ...
def isalive(self) -> bool: ...
def read(self, size: int = 1024) -> str: ...
def write(self, s: str) -> int: ...
def terminate(self, force: bool = False) -> bool: ...
def kill(self, sig: int | None = None) -> None: ...
def close(self, force: bool = False) -> None: ...
def wait(self) -> int | None: ...
def fileno(self) -> int: ...
def isatty(self) -> bool: ...
def setwinsize(self, rows: int, cols: int) -> None: ...
def getwinsize(self) -> tuple[int, int]: ...