ptylink 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. ptylink-0.1.0/.github/dependabot.yml +10 -0
  2. ptylink-0.1.0/.github/workflows/ci.yml +49 -0
  3. ptylink-0.1.0/.github/workflows/publish.yml +19 -0
  4. ptylink-0.1.0/.gitignore +16 -0
  5. ptylink-0.1.0/CHANGELOG.md +33 -0
  6. ptylink-0.1.0/PKG-INFO +291 -0
  7. ptylink-0.1.0/PLAN.md +266 -0
  8. ptylink-0.1.0/README.md +260 -0
  9. ptylink-0.1.0/benchmarks/RESULTS.md +14 -0
  10. ptylink-0.1.0/benchmarks/bench_ptylink.py +107 -0
  11. ptylink-0.1.0/pyproject.toml +79 -0
  12. ptylink-0.1.0/pyrightconfig.json +6 -0
  13. ptylink-0.1.0/src/ptylink/__init__.py +50 -0
  14. ptylink-0.1.0/src/ptylink/_async.py +402 -0
  15. ptylink-0.1.0/src/ptylink/_compat.py +46 -0
  16. ptylink-0.1.0/src/ptylink/_errors.py +48 -0
  17. ptylink-0.1.0/src/ptylink/_expect.py +177 -0
  18. ptylink-0.1.0/src/ptylink/_interact.py +64 -0
  19. ptylink-0.1.0/src/ptylink/_popen.py +288 -0
  20. ptylink-0.1.0/src/ptylink/_pty.py +235 -0
  21. ptylink-0.1.0/src/ptylink/_run.py +86 -0
  22. ptylink-0.1.0/src/ptylink/_screen.py +30 -0
  23. ptylink-0.1.0/src/ptylink/_spawn.py +363 -0
  24. ptylink-0.1.0/src/ptylink/_ssh.py +136 -0
  25. ptylink-0.1.0/src/ptylink/_types.py +76 -0
  26. ptylink-0.1.0/src/ptylink/compat.py +7 -0
  27. ptylink-0.1.0/src/ptylink/py.typed +0 -0
  28. ptylink-0.1.0/tests/test_async.py +131 -0
  29. ptylink-0.1.0/tests/test_compat.py +179 -0
  30. ptylink-0.1.0/tests/test_errors.py +116 -0
  31. ptylink-0.1.0/tests/test_expect.py +280 -0
  32. ptylink-0.1.0/tests/test_popen.py +94 -0
  33. ptylink-0.1.0/tests/test_pty.py +157 -0
  34. ptylink-0.1.0/tests/test_run.py +47 -0
  35. ptylink-0.1.0/tests/test_screen.py +48 -0
  36. ptylink-0.1.0/tests/test_spawn.py +257 -0
  37. ptylink-0.1.0/tests/test_spawn_advanced.py +64 -0
  38. ptylink-0.1.0/tests/test_ssh.py +71 -0
  39. ptylink-0.1.0/uv.lock +400 -0
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ - package-ecosystem: "pip"
8
+ directory: "/"
9
+ schedule:
10
+ interval: "weekly"
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, macos-latest]
16
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v5
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ run: uv python install ${{ matrix.python-version }}
22
+ - name: Install dependencies
23
+ run: uv sync --dev
24
+ - name: Run tests
25
+ run: uv run python -m pytest tests/ -q
26
+
27
+ typecheck:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: astral-sh/setup-uv@v5
32
+ - name: Install dependencies
33
+ run: uv sync --dev
34
+ - name: mypy --strict
35
+ run: uv run mypy --strict src/ptylink/
36
+ - name: pyright
37
+ run: uv run pyright src/ptylink/
38
+
39
+ lint:
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: astral-sh/setup-uv@v5
44
+ - name: Install dependencies
45
+ run: uv sync --dev
46
+ - name: Ruff check
47
+ run: uv run ruff check src/ tests/
48
+ - name: Ruff format check
49
+ run: uv run ruff format --check src/ tests/
@@ -0,0 +1,19 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v5
16
+ - name: Build distribution
17
+ run: uv build
18
+ - name: Publish to PyPI
19
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ *.egg
6
+ dist/
7
+ build/
8
+ .venv/
9
+ .env
10
+ .mypy_cache/
11
+ .pyright/
12
+ .ruff_cache/
13
+ .pytest_cache/
14
+ htmlcov/
15
+ coverage/
16
+ *.so
@@ -0,0 +1,33 @@
1
+ # Changelog
2
+
3
+ > **Note:** This package was previously published as **tether**. It was renamed to **ptylink** after v0.1.0.
4
+ > If you were using `tether`, update your dependency to `ptylink` and replace any `TetherError` references with `PtylinkError`.
5
+
6
+ ## v0.1.0 (2026-03-13)
7
+
8
+ Initial release as `tether` — modern drop-in replacement for pexpect. Renamed to `ptylink` post-release.
9
+
10
+ ### Features
11
+
12
+ - `Spawn` class with PTY-based process interaction
13
+ - `AsyncSpawn`: native `async/await` (not `async_=True` hack)
14
+ - `PopenSpawn`: pipe-based spawn for environments without PTY
15
+ - `SSHSession`: SSH login/command helper built on Spawn
16
+ - `run()` high-level function with events support
17
+ - Pattern matching: regex, exact string, EOF, TIMEOUT sentinels
18
+ - `strip_ansi()` and `has_ansi()` ANSI escape handling
19
+ - `interact()` standalone function with input/output filters
20
+ - pexpect compatibility shim (`ptylink.compat`)
21
+ - Zero dependencies
22
+ - Full type annotations — `mypy --strict` + `pyright strict` clean
23
+ - Python 3.10+
24
+
25
+ ### Bug fixes vs pexpect
26
+
27
+ - `before` attribute no longer contains leaked `sendline()` echo (#821)
28
+ - No Python 3.12+ `ResourceWarning` on fork (#817)
29
+ - No deprecated `asyncio.coroutine` usage (#677)
30
+
31
+ ### Performance
32
+
33
+ - 3.6–95× faster than pexpect across all benchmarked operations
ptylink-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: ptylink
3
+ Version: 0.1.0
4
+ Summary: Modern process interaction library — expect-style automation with PTY support
5
+ Project-URL: Homepage, https://github.com/agentine/ptylink
6
+ Project-URL: Repository, https://github.com/agentine/ptylink
7
+ Project-URL: Issues, https://github.com/agentine/ptylink/issues
8
+ Author: Agentine
9
+ License-Expression: ISC
10
+ Keywords: automation,expect,pexpect,process,pty,terminal
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: System :: Systems Administration
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.8; extra == 'dev'
26
+ Requires-Dist: pexpect>=4.9.0; extra == 'dev'
27
+ Requires-Dist: pyright>=1.1; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.3; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # ptylink
33
+
34
+ [![CI](https://github.com/agentine/ptylink/actions/workflows/ci.yml/badge.svg)](https://github.com/agentine/ptylink/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/ptylink)](https://pypi.org/project/ptylink/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/ptylink)](https://pypi.org/project/ptylink/)
37
+
38
+ Modern process interaction library for Python — expect-style automation with PTY support.
39
+
40
+ Drop-in replacement for [pexpect](https://github.com/pexpect/pexpect) with full type annotations, native async/await, and zero dependencies.
41
+
42
+ ## Why ptylink?
43
+
44
+ | | pexpect | ptylink |
45
+ |---|---|---|
46
+ | **Type annotations** | No | Full (`mypy --strict` + `pyright strict`) |
47
+ | **Async support** | Deprecated `@asyncio.coroutine` | Native `async/await` via `AsyncSpawn` |
48
+ | **Python 3.12+ fork warning** | Yes (#817) | Fixed |
49
+ | **`before` leaks sendline echo** | Yes (#821) | Fixed |
50
+ | **Dependencies** | `ptyprocess` | Zero |
51
+ | **Performance** | Baseline | 3.6–95× faster |
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ pip install ptylink
57
+ ```
58
+
59
+ Requires Python 3.10+.
60
+
61
+ ## Quick Start
62
+
63
+ ```python
64
+ import ptylink
65
+
66
+ with ptylink.spawn("python3") as child:
67
+ child.expect(">>> ")
68
+ child.sendline("print(42)")
69
+ child.expect("42")
70
+ print(child.before) # text before the match
71
+ ```
72
+
73
+ ## Context Manager
74
+
75
+ ```python
76
+ from ptylink import Spawn
77
+
78
+ with Spawn("ssh user@host") as child:
79
+ child.expect("password:")
80
+ child.sendline("secret")
81
+ child.expect(r"\$ ")
82
+ child.sendline("ls")
83
+ child.expect(r"\$ ")
84
+ print(child.before)
85
+ ```
86
+
87
+ ## Async Usage
88
+
89
+ ```python
90
+ import asyncio
91
+ from ptylink import AsyncSpawn
92
+
93
+ async def main():
94
+ async with AsyncSpawn("python3") as child:
95
+ await child.expect(">>> ")
96
+ await child.sendline("1 + 1")
97
+ await child.expect("2")
98
+
99
+ asyncio.run(main())
100
+ ```
101
+
102
+ ## Pipe-Based (No PTY)
103
+
104
+ For environments without PTY support (e.g. Windows):
105
+
106
+ ```python
107
+ from ptylink import PopenSpawn
108
+
109
+ with PopenSpawn("echo hello") as child:
110
+ child.expect("hello")
111
+ ```
112
+
113
+ ## High-Level `run()`
114
+
115
+ ```python
116
+ from ptylink import run
117
+
118
+ # Simple command
119
+ output = run("ls -la")
120
+
121
+ # With exit status
122
+ output, status = run("make test", withexitstatus=True)
123
+
124
+ # Interactive with events
125
+ output = run(
126
+ "sudo apt install foo",
127
+ events={"password:": "secret\n"},
128
+ )
129
+ ```
130
+
131
+ ## SSH Sessions
132
+
133
+ ```python
134
+ from ptylink import SSHSession
135
+
136
+ with SSHSession("server.example.com", username="admin") as ssh:
137
+ ssh.login(password="secret")
138
+ output = ssh.run("uname -a")
139
+ print(output)
140
+ ```
141
+
142
+ ## Pattern Matching
143
+
144
+ ```python
145
+ import re
146
+ from ptylink import Spawn, EOF_TYPE, TIMEOUT_TYPE
147
+
148
+ with Spawn("some_program") as child:
149
+ # String patterns (auto-escaped)
150
+ child.expect("login:")
151
+
152
+ # Regex patterns
153
+ child.expect(re.compile(r"[\$#] "))
154
+
155
+ # Multiple patterns — returns index of match
156
+ idx = child.expect_list(["error", "success", EOF_TYPE])
157
+ if idx == 0:
158
+ print("Error:", child.after)
159
+ elif idx == 1:
160
+ print("Success!")
161
+ elif idx == 2:
162
+ print("Process ended")
163
+ ```
164
+
165
+ ## API Reference
166
+
167
+ ### Classes and Functions
168
+
169
+ - **`Spawn(command, *, timeout=30, encoding='utf-8', env=None, cwd=None)`** — PTY-based process interaction
170
+ - **`spawn(command, *, timeout=30, encoding='utf-8')`** — Factory function; returns a `Spawn` instance
171
+ - **`AsyncSpawn(command, ...)`** — Async version of Spawn
172
+ - **`PopenSpawn(command, ...)`** — Pipe-based (no PTY) process interaction
173
+ - **`SSHSession(server, *, username=None, port=22, password=None)`** — SSH session helper
174
+
175
+ ### Spawn Methods
176
+
177
+ | Method | Description |
178
+ |--------|-------------|
179
+ | `expect(pattern, *, timeout=-1)` | Wait for pattern in output |
180
+ | `expect_exact(pattern, *, timeout=-1)` | Wait for exact string |
181
+ | `expect_list(patterns, *, timeout=-1)` | Wait for any pattern, return index |
182
+ | `send(s)` | Send string to process |
183
+ | `sendline(s='')` | Send string + newline |
184
+ | `sendcontrol(char)` | Send control character (e.g. `'c'` for Ctrl-C) |
185
+ | `sendeof()` | Send EOF (Ctrl-D) |
186
+ | `read(size=-1)` | Read from process output |
187
+ | `readline()` | Read a single line |
188
+ | `isalive()` | Check if process is running |
189
+ | `wait()` | Wait for exit, return exit code |
190
+ | `close(force=True)` | Close process and PTY |
191
+ | `setwinsize(rows, cols)` | Set terminal dimensions |
192
+ | `interact()` | Interactive passthrough mode |
193
+
194
+ ### Attributes
195
+
196
+ - `before` — Text before the last match
197
+ - `after` — Text of the last match
198
+ - `match` — Match object or string from last expect
199
+
200
+ ### Exceptions
201
+
202
+ - `PtylinkError` — Base exception
203
+ - `Timeout` — Expect timed out
204
+ - `EOF` — Process closed output
205
+ - `ExitStatus` — Process exited with non-zero status
206
+
207
+ ### Sentinels
208
+
209
+ - `EOF_TYPE` — Use in pattern lists to match EOF without raising
210
+ - `TIMEOUT_TYPE` — Use in pattern lists to match timeout without raising
211
+
212
+ ## ANSI Utilities
213
+
214
+ ```python
215
+ from ptylink import strip_ansi, has_ansi
216
+
217
+ clean = strip_ansi("\x1b[31mred text\x1b[0m") # "red text"
218
+ has_ansi("\x1b[1mbold\x1b[0m") # True
219
+ ```
220
+
221
+ ## Migrating from tether
222
+
223
+ This package was previously named **tether**. To upgrade:
224
+
225
+ ```bash
226
+ pip uninstall tether
227
+ pip install ptylink
228
+ ```
229
+
230
+ Then update your imports and any exception references:
231
+
232
+ ```python
233
+ # Before
234
+ import tether
235
+ from tether import TetherError
236
+
237
+ # After
238
+ import ptylink
239
+ from ptylink import PtylinkError
240
+ ```
241
+
242
+ All other APIs are identical.
243
+
244
+ ## pexpect Migration Guide
245
+
246
+ ### Zero-Change Migration
247
+
248
+ ```python
249
+ # Before
250
+ import pexpect
251
+
252
+ # After — just change the import
253
+ import ptylink.compat as pexpect
254
+ ```
255
+
256
+ All pexpect names are available: `spawn`, `run`, `EOF`, `TIMEOUT`, `pxssh`.
257
+
258
+ ### Manual Migration
259
+
260
+ | pexpect | ptylink |
261
+ |---------|--------|
262
+ | `pexpect.spawn(cmd)` | `ptylink.Spawn(cmd)` |
263
+ | `pexpect.run(cmd)` | `ptylink.run(cmd)` |
264
+ | `pexpect.EOF` | `ptylink.EOF_TYPE` |
265
+ | `pexpect.TIMEOUT` | `ptylink.TIMEOUT_TYPE` |
266
+ | `pexpect.pxssh.pxssh()` | `ptylink.SSHSession()` |
267
+ | `pexpect.spawn(cmd, async_=True)` | `ptylink.AsyncSpawn(cmd)` |
268
+
269
+ ### Breaking Changes
270
+
271
+ - `EOF` and `TIMEOUT` are sentinel types, not exception classes. Use `EOF_TYPE` and `TIMEOUT_TYPE` in pattern lists.
272
+ - `async_=True` parameter is removed. Use `AsyncSpawn` instead.
273
+ - `before` attribute no longer contains leaked `sendline()` echo text.
274
+
275
+ ## Fixes from pexpect
276
+
277
+ - **#821** — `before` attribute no longer contains echoed `sendline()` input
278
+ - **#817** — No `ResourceWarning` from `os.fork()` on Python 3.12+
279
+ - **#677** — No deprecated `@asyncio.coroutine` usage; native `async def` throughout
280
+
281
+ ## Benchmarks
282
+
283
+ | Operation | ptylink (us/op) | pexpect (us/op) | Speedup |
284
+ |-----------|---------------:|----------------:|--------:|
285
+ | spawn+expect (echo) | 3,002 | 285,821 | 95x |
286
+ | spawn+expect (python) | 84,312 | 307,303 | 3.6x |
287
+ | run (echo) | 3,352 | 163,946 | 49x |
288
+
289
+ ## License
290
+
291
+ ISC
ptylink-0.1.0/PLAN.md ADDED
@@ -0,0 +1,266 @@
1
+ # ptylink — Implementation Plan
2
+
3
+ **Target:** Replace `pexpect` (pexpect/pexpect)
4
+ **Package name:** `ptylink` (verified available on PyPI 2026-03-13)
5
+ **License:** ISC
6
+ **Python:** >= 3.10
7
+ **Dependencies:** Zero (pure Python, PTY handling built-in)
8
+
9
+ ---
10
+
11
+ ## Problem Statement
12
+
13
+ `pexpect` is the dominant Python library for controlling interactive programs in a pseudo-terminal with **149M downloads/month** (PyPI #152). It has:
14
+
15
+ - **Two primary maintainers** (takluyver: 432 commits, jquast: 320 commits) — high bus factor risk
16
+ - **Stale releases** — last release v4.9 on November 25, 2023 (2+ years ago); previous release v4.8 was January 2020
17
+ - **165 open issues** including deprecated asyncio.coroutine usage (#677), Python 3.12 fork warnings (#817), ANSI leaks on macOS 3.14 (#824), bare except clauses (#826)
18
+ - **Stale pull requests** — PRs from 2024-2026 unmerged (e.g., #826 basic cleanup, #822 dependency bump)
19
+ - **No type annotations** — no py.typed, no mypy/pyright support
20
+ - **Weak async support** — only `async_=True` parameter hack, uses deprecated `@asyncio.coroutine`
21
+ - **Separate ptyprocess dependency** — also stale (last release v0.7.0, December 2020)
22
+ - **Poor Windows support** — popen_spawn fallback only, no ConPTY
23
+ - **No funding** — not on Tidelift, no GitHub Sponsors, no corporate backing
24
+ - **No well-known replacement** — wexpect is Windows-only; no modern async-first expect library exists
25
+
26
+ ## Scope
27
+
28
+ Modern process interaction library for controlling interactive CLI programs:
29
+
30
+ 1. **Process spawning** — PTY allocation (Unix), ConPTY (Windows 10+), Popen fallback
31
+ 2. **Pattern matching** — expect patterns on process output (regex, literal, EOF, TIMEOUT)
32
+ 3. **Input sending** — send, sendline, sendcontrol, sendeof, sendintr
33
+ 4. **Timeout handling** — per-operation and default timeouts
34
+ 5. **Session state** — before/after/match attributes for matched content
35
+ 6. **Interactive mode** — passthrough for manual interaction
36
+ 7. **High-level API** — run() for simple command execution with expect
37
+ 8. **SSH helper** — SSH session management (login, command execution)
38
+ 9. **Async support** — native async/await for all operations
39
+ 10. **pexpect compatibility** — drop-in shim for existing code
40
+
41
+ ## Architecture Overview
42
+
43
+ ```
44
+ src/ptylink/
45
+ ├── __init__.py # Public API exports
46
+ ├── py.typed # PEP 561 marker
47
+ ├── _types.py # Type aliases, protocols, sentinel types
48
+ ├── _errors.py # Exception hierarchy (Timeout, EOF, ExitStatus)
49
+ ├── _pty.py # PTY allocation and management (replaces ptyprocess)
50
+ ├── _spawn.py # Spawn class — core process interaction
51
+ ├── _expect.py # Pattern matching engine (compile, search, match)
52
+ ├── _interact.py # Interactive passthrough mode
53
+ ├── _run.py # High-level run() function
54
+ ├── _popen.py # PopenSpawn — non-PTY spawn (Windows, pipes)
55
+ ├── _ssh.py # SSH session helper (login, prompts, commands)
56
+ ├── _screen.py # ANSI escape sequence handling
57
+ ├── _async.py # AsyncSpawn — native async/await spawn
58
+ └── _compat.py # pexpect drop-in compatibility shim
59
+ ```
60
+
61
+ ### Key Design Decisions
62
+
63
+ - **Private modules, public API via `__init__.py`** — all internal modules prefixed with `_`, public surface is `ptylink.spawn()`, `ptylink.run()`, `ptylink.Spawn`, etc.
64
+ - **Async-first internals** — core I/O uses asyncio; sync API wraps async with `asyncio.run()` / event loop
65
+ - **Built-in PTY** — PTY handling built directly into the library (no ptyprocess dependency)
66
+ - **Context manager support** — `with ptylink.spawn("cmd") as child:` for automatic cleanup
67
+ - **Compiled patterns** — expect patterns compiled once and reused
68
+ - **Sentinel types** — `EOF` and `TIMEOUT` are proper singleton types, not magic integers
69
+ - **Modern Python** — 3.10+ required; uses match statements, `|` union types, slots
70
+
71
+ ## Major Components
72
+
73
+ ### 1. Exception Hierarchy (`_errors.py`)
74
+
75
+ ```python
76
+ class PtylinkError(Exception): ... # Base
77
+ class Timeout(PtylinkError): ... # Expect timeout
78
+ class EOF(PtylinkError): ... # Process closed output
79
+ class ExitStatus(PtylinkError): # Process exited with error
80
+ status: int
81
+ signal: int | None
82
+ ```
83
+
84
+ **Fix over pexpect:** Clear exception types with proper attributes. pexpect EOF/TIMEOUT are exception classes AND sentinel values — ptylink separates these concerns.
85
+
86
+ ### 2. PTY Management (`_pty.py`)
87
+
88
+ ```python
89
+ class PtyProcess:
90
+ pid: int
91
+ fd: int
92
+
93
+ @classmethod
94
+ def spawn(cls, argv: list[str], ...) -> PtyProcess: ...
95
+ def read(self, size: int = 1024) -> bytes: ...
96
+ def write(self, data: bytes) -> int: ...
97
+ def setwinsize(self, rows: int, cols: int) -> None: ...
98
+ def waitpid(self) -> tuple[int, int]: ...
99
+ def terminate(self, force: bool = False) -> bool: ...
100
+ def isalive(self) -> bool: ...
101
+ ```
102
+
103
+ Replaces ptyprocess with built-in PTY handling. Uses `pty.openpty()` + `os.fork()` on Unix with proper signal handling.
104
+
105
+ **Fix over pexpect/ptyprocess:** Handles Python 3.12+ fork-with-threads warning (#817). Uses `os.login_tty()` on Python 3.13+.
106
+
107
+ ### 3. Spawn Class (`_spawn.py`)
108
+
109
+ ```python
110
+ class Spawn:
111
+ before: str # Text before last match
112
+ after: str # Text that matched
113
+ match: re.Match | str | None
114
+
115
+ def __init__(self, command: str, *, timeout: float = 30, encoding: str = "utf-8", ...) -> None: ...
116
+ def __enter__(self) -> Spawn: ...
117
+ def __exit__(self, ...) -> None: ...
118
+ def expect(self, pattern: Pattern, *, timeout: float = -1) -> int: ...
119
+ def expect_exact(self, pattern: str | list[str], ...) -> int: ...
120
+ def expect_list(self, patterns: list[Pattern], ...) -> int: ...
121
+ def send(self, s: str) -> int: ...
122
+ def sendline(self, s: str = "") -> int: ...
123
+ def sendcontrol(self, char: str) -> int: ...
124
+ def sendeof(self) -> None: ...
125
+ def sendintr(self) -> None: ...
126
+ def read(self, size: int = -1) -> str: ...
127
+ def readline(self) -> str: ...
128
+ def isalive(self) -> bool: ...
129
+ def wait(self) -> int: ...
130
+ def close(self, force: bool = True) -> None: ...
131
+ def terminate(self, force: bool = False) -> bool: ...
132
+ def interact(self, ...) -> None: ...
133
+ ```
134
+
135
+ **Fixes over pexpect:**
136
+ - Context manager for automatic cleanup
137
+ - `before` never contains input from `sendline` (#821)
138
+ - No bare except clauses (#826)
139
+ - Proper typing on all methods
140
+
141
+ ### 4. Pattern Matching (`_expect.py`)
142
+
143
+ ```python
144
+ Pattern = str | re.Pattern[str] | type[EOF_TYPE] | type[TIMEOUT_TYPE]
145
+
146
+ def compile_pattern(pattern: Pattern) -> CompiledPattern: ...
147
+ def expect_loop(spawn: Spawn, patterns: list[CompiledPattern], timeout: float) -> int: ...
148
+ ```
149
+
150
+ Patterns are compiled once. The expect loop uses `select.select()` (Unix) or polling for non-blocking reads.
151
+
152
+ ### 5. Async Spawn (`_async.py`)
153
+
154
+ ```python
155
+ class AsyncSpawn:
156
+ async def expect(self, pattern: Pattern, *, timeout: float = -1) -> int: ...
157
+ async def send(self, s: str) -> int: ...
158
+ async def sendline(self, s: str = "") -> int: ...
159
+ async def read(self, size: int = -1) -> str: ...
160
+ async def __aenter__(self) -> AsyncSpawn: ...
161
+ async def __aexit__(self, ...) -> None: ...
162
+ ```
163
+
164
+ **Fix over pexpect:** Native async/await, not retrofitted `async_=True` parameter. Uses `asyncio.get_event_loop().add_reader()` for non-blocking PTY reads. No deprecated `@asyncio.coroutine`.
165
+
166
+ ### 6. SSH Helper (`_ssh.py`)
167
+
168
+ ```python
169
+ class SSHSession:
170
+ def __init__(self, server: str, *, username: str | None = None, port: int = 22, ...) -> None: ...
171
+ def login(self, ...) -> None: ...
172
+ def prompt(self, timeout: float = -1) -> bool: ...
173
+ def run(self, command: str, *, timeout: float = -1) -> str: ...
174
+ def logout(self) -> None: ...
175
+ ```
176
+
177
+ Wraps Spawn for SSH connections with login automation and prompt detection.
178
+
179
+ ### 7. Popen Spawn (`_popen.py`)
180
+
181
+ ```python
182
+ class PopenSpawn:
183
+ """Non-PTY spawn using subprocess.Popen. Works on Windows."""
184
+ # Same interface as Spawn but uses pipes instead of PTY
185
+ ```
186
+
187
+ ### 8. Compatibility Shim (`_compat.py`)
188
+
189
+ Drop-in replacement for code using `import pexpect`:
190
+
191
+ ```python
192
+ # ptylink.compat provides all pexpect public names
193
+ from ptylink.compat import spawn, run, EOF, TIMEOUT
194
+ from ptylink.compat import pxssh # maps to ptylink.SSHSession
195
+ ```
196
+
197
+ ## Phases
198
+
199
+ ### Phase 1: Core — PTY, Spawn, Expect (Priority: highest)
200
+ - Exception hierarchy
201
+ - PTY process management (fork, pty, signals)
202
+ - Spawn class with send/sendline/expect/expect_exact
203
+ - Pattern matching engine (regex, literal, EOF, TIMEOUT)
204
+ - Timeout handling
205
+ - Context manager (with/as)
206
+ - before/after/match state
207
+ - isalive/wait/close/terminate
208
+ - Unit tests for all core functionality
209
+
210
+ ### Phase 2: Advanced Features
211
+ - expect_list (multiple pattern matching)
212
+ - sendcontrol/sendeof/sendintr
213
+ - interact() mode
214
+ - run() high-level function
215
+ - Logging/debugging support
216
+ - Window size management (setwinsize)
217
+ - ANSI escape sequence handling
218
+ - Unit tests for advanced features
219
+
220
+ ### Phase 3: Extensions
221
+ - AsyncSpawn — native async/await
222
+ - PopenSpawn — non-PTY (pipes, Windows support)
223
+ - SSHSession — SSH login/command helper
224
+ - Unit tests for extensions
225
+
226
+ ### Phase 4: Quality & Compatibility
227
+ - pexpect compatibility shim
228
+ - Cross-verification tests (run same scenarios with pexpect and ptylink)
229
+ - mypy --strict and pyright clean
230
+ - Benchmarks vs pexpect
231
+ - CI (GitHub Actions: Python 3.10, 3.11, 3.12, 3.13, ubuntu + macos)
232
+ - pyproject.toml with full metadata
233
+
234
+ ### Phase 5: Documentation & Release
235
+ - README.md with migration guide from pexpect
236
+ - CHANGELOG.md
237
+ - API reference (docstrings, all public functions)
238
+ - Release v0.1.0 to PyPI
239
+
240
+ ## Deliverables
241
+
242
+ - `projects/ptylink/` — complete Python package
243
+ - Process spawning with PTY allocation
244
+ - Expect-style pattern matching on process output
245
+ - Native async/await support
246
+ - SSH session helper
247
+ - PopenSpawn for Windows/pipe-based operation
248
+ - pexpect compatibility shim
249
+ - 100% type-annotated, mypy/pyright strict clean
250
+ - Comprehensive test suite with cross-verification
251
+ - Benchmarks vs pexpect
252
+ - CI pipeline
253
+ - README with pexpect migration guide
254
+
255
+ ## Success Criteria
256
+
257
+ - `ptylink.spawn("python3")` spawns process with PTY, expect/send works
258
+ - `child.expect(r">>> ")` matches Python REPL prompt
259
+ - `child.sendline("print('hello')")` sends input correctly
260
+ - `child.before` contains output without leaked input (#821 fix)
261
+ - `async with ptylink.AsyncSpawn("cmd") as child:` works with native async
262
+ - `with ptylink.spawn("cmd") as child:` auto-cleans up on exit
263
+ - No Python 3.12+ fork warnings (#817)
264
+ - mypy --strict passes with zero errors
265
+ - pytest passes on Python 3.10, 3.11, 3.12, 3.13
266
+ - Benchmark within 2x of pexpect performance (targeting parity)