pywinpty 3.0.3rc0__cp314-cp314t-win_arm64.whl

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.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: pywinpty
3
+ Version: 3.0.3rc0
4
+ Classifier: Development Status :: 5 - Production/Stable
5
+ Classifier: Programming Language :: Python
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: Microsoft :: Windows
8
+ Classifier: Programming Language :: Python :: Free Threading
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Terminals
11
+ Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
12
+ Classifier: Programming Language :: Rust
13
+ License-File: LICENSE.txt
14
+ Summary: Pseudo terminal support for Windows from Python.
15
+ Keywords: PTY,Windows,pseudo-terminal,PyO3
16
+ Author: Edgar Margffoy
17
+ Author-email: andfoy@gmail.com
18
+ Maintainer-email: Edgar Margffoy <andfoy@gmail.com>
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
21
+ Project-URL: Changelog, https://github.com/andfoy/pywinpty/blob/main/CHANGELOG.md
22
+ Project-URL: Repository, https://github.com/andfoy/pywinpty
23
+
24
+ # PyWinpty: Pseudoterminals for Windows in Python
25
+
26
+ [![Project License - MIT](https://img.shields.io/pypi/l/pywinpty.svg)](./LICENSE.txt)
27
+ [![pypi version](https://img.shields.io/pypi/v/pywinpty.svg)](https://pypi.org/project/pywinpty/)
28
+ [![conda version](https://img.shields.io/conda/vn/conda-forge/pywinpty.svg)](https://www.anaconda.com/download/)
29
+ [![download count](https://img.shields.io/conda/dn/conda-forge/pywinpty.svg)](https://www.anaconda.com/download/)
30
+ [![Downloads](https://pepy.tech/badge/pywinpty)](https://pepy.tech/project/pywinpty)
31
+ [![PyPI status](https://img.shields.io/pypi/status/pywinpty.svg)](https://github.com/spyder-ide/pywinpty)
32
+ [![Windows tests](https://github.com/andfoy/pywinpty/actions/workflows/windows_build.yml/badge.svg)](https://github.com/andfoy/pywinpty/actions/workflows/windows_build.yml)
33
+
34
+ *Copyright © 2017–2022 Spyder Project Contributors*
35
+ *Copyright © 2022– Edgar Andrés Margffoy Tuay*
36
+
37
+
38
+ ## Overview
39
+
40
+ PyWinpty allows creating and communicating with Windows processes that receive input and print outputs via console input and output pipes. PyWinpty supports both the native [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) interface and the previous, fallback [winpty](https://github.com/rprichard/winpty) library.
41
+
42
+
43
+ ## Dependencies
44
+ To compile pywinpty sources, you must have [Rust](https://rustup.rs/) installed.
45
+ Optionally, you can also have Winpty's C header and library files available on your include path.
46
+
47
+
48
+ ## Installation
49
+ You can install this library by using conda or pip package managers, as it follows:
50
+
51
+ Using conda (Recommended):
52
+ ```bash
53
+ conda install pywinpty
54
+ ```
55
+
56
+ Using pip:
57
+ ```bash
58
+ pip install pywinpty
59
+ ```
60
+
61
+ ## Building from source
62
+
63
+ To build from sources, you will require both a working stable or nightly Rust toolchain with
64
+ target `x86_64-pc-windows-msvc`, which can be installed using [rustup](https://rustup.rs/).
65
+
66
+ Optionally, this library can be linked against winpty library, which you can install using conda-forge:
67
+
68
+ ```batch
69
+ conda install winpty -c conda-forge
70
+ ```
71
+
72
+ If you don't want to use conda, you will need to have the winpty binaries and headers available on your PATH.
73
+
74
+ Finally, pywinpty uses [Maturin](https://github.com/PyO3/maturin) as the build backend, which can be installed using `pip`:
75
+
76
+ ```batch
77
+ pip install maturin
78
+ ```
79
+
80
+ To test your compilation environment settings, you can build pywinpty sources locally, by
81
+ executing:
82
+
83
+ ```bash
84
+ maturin develop
85
+ ```
86
+
87
+ This package depends on the following Rust crates:
88
+
89
+ * [PyO3](https://github.com/PyO3/pyo3): Library used to produce Python bindings from Rust code.
90
+ * [WinPTY-rs](https://github.com/andfoy/winpty-rs): Create and spawn processes inside a pseudoterminal in Windows from Rust.
91
+ * [Maturin](https://github.com/PyO3/maturin): Build system to build and publish Rust-based Python packages.
92
+
93
+ ## Package usage
94
+ Pywinpty offers a single python wrapper around winpty library functions.
95
+ This implies that using a single object (``winpty.PTY``) it is possible to access to all functionality, as it follows:
96
+
97
+ ```python
98
+ # High level usage using `spawn`
99
+ from winpty import PtyProcess
100
+
101
+ proc = PtyProcess.spawn('python')
102
+ proc.write('print("hello, world!")\r\n')
103
+ proc.write('exit()\r\n')
104
+ while proc.isalive():
105
+ print(proc.readline())
106
+
107
+ # Low level usage using the raw `PTY` object
108
+ from winpty import PTY
109
+
110
+ # Start a new winpty-agent process of size (cols, rows)
111
+ cols, rows = 80, 25
112
+ process = PTY(cols, rows)
113
+
114
+ # Spawn a new console process, e.g., CMD
115
+ process.spawn(br'C:\windows\system32\cmd.exe')
116
+
117
+ # Read console output (Unicode)
118
+ process.read()
119
+
120
+ # Write input to console (Unicode)
121
+ process.write(b'Text')
122
+
123
+ # Resize console size
124
+ new_cols, new_rows = 90, 30
125
+ process.set_size(new_cols, new_rows)
126
+
127
+ # Know if the process is alive
128
+ alive = process.isalive()
129
+
130
+ # End winpty-agent process
131
+ del process
132
+ ```
133
+
134
+ ## Running tests
135
+ We use pytest to run tests as it follows (after calling ``maturin develop``), the test suite depends
136
+ on pytest-lazy-fixture, which can be installed via pip:
137
+
138
+ ```batch
139
+ pip install pytest pytest-lazy-fixture flaky
140
+ ```
141
+
142
+ All the tests can be executed using the following command
143
+
144
+ ```bash
145
+ python runtests.py
146
+ ```
147
+
148
+
149
+ ## Changelog
150
+ Visit our [CHANGELOG](CHANGELOG.md) file to learn more about our new features and improvements.
151
+
152
+
153
+ ## Contribution guidelines
154
+ We follow PEP8 and PEP257 for pure python packages and Rust to compile extensions. We use MyPy type annotations for all functions and classes declared on this package. Feel free to send a PR or create an issue if you have any problem/question.
155
+
156
+
157
+ ## Security contact information
158
+
159
+ To report a security vulnerability, please use the
160
+ [Tidelift security contact](https://tidelift.com/security).
161
+ Tidelift will coordinate the fix and disclosure.
162
+
@@ -0,0 +1,12 @@
1
+ pywinpty-3.0.3rc0.dist-info\METADATA,sha256=YfZuBz_mHTiQ77Jef7J8h6tSdjFNas0qYTrODQi8TYs,5893
2
+ pywinpty-3.0.3rc0.dist-info\WHEEL,sha256=j6TxrWL6eSoRGgXGBPXV8d7UTQITaoAmQJXUnXY5IaU,98
3
+ pywinpty-3.0.3rc0.dist-info\licenses\LICENSE.txt,sha256=-HjUdn-a0uQ9MIPvoAIBsADOk32e6GJuALpccqrJUeI,1088
4
+ winpty\__init__.py,sha256=laTi9sLjCLycyaQYO3wMvhZNIK2vh_pOvGTJT2Od4Qs,402
5
+ winpty\enums.py,sha256=KAm7XJFPk7nMWwUJxvYHlql92cr6y-Y24IHK3fc0JDU,1860
6
+ winpty\ptyprocess.py,sha256=hucCVfBridHRc4IPiEFrEkdFhU-UKSQbyZUa97EceDc,12209
7
+ winpty\tests\__init__.py,sha256=fzb9cDnPt2R3b_rWh6sqDgIbiQOZtBlfsV1aq-ULT2Q,53
8
+ winpty\tests\test_pty.py,sha256=AXujQybXhKr0t0UKpNBSqeAfsm4fHoU_ognbiocBF5I,3947
9
+ winpty\tests\test_ptyprocess.py,sha256=MeGp_P-zkseEp0iXciWh9Fa548hYGdvlcaHy_WNEwXo,6112
10
+ winpty\winpty.cp314t-win_arm64.pyd,sha256=sRJSxTbK71FGY8Nt-rrnDx2QRIwJQq5xlxAerJHOCW8,524288
11
+ winpty\winpty.pyi,sha256=-VenowCvy8tjIZUCvvb5VZw5R1c6ATXEUfsiEyIZwjE,1176
12
+ pywinpty-3.0.3rc0.dist-info\RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.11.5)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314t-win_arm64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Spyder IDE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
winpty/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Pywinpty
4
+ ========
5
+ This package provides low and high level APIs to create
6
+ pseudo terminals in Windows.
7
+ """
8
+
9
+ # Local imports
10
+ from .winpty import PTY, WinptyError, __version__
11
+ from .ptyprocess import PtyProcess
12
+ from .enums import Backend, Encoding, MouseMode, AgentConfig
13
+
14
+
15
+ PTY
16
+ PtyProcess
17
+ Backend
18
+ Encoding
19
+ MouseMode
20
+ AgentConfig
21
+ WinptyError
22
+
23
+ __version__
winpty/enums.py ADDED
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """General constants used to spawn a PTY."""
4
+
5
+
6
+ class Backend:
7
+ """Available PTY backends."""
8
+ ConPTY = 0
9
+ WinPTY = 1
10
+
11
+
12
+ class Encoding:
13
+ """Available byte encodings to communicate with a PTY."""
14
+ UTF8 = 'utf-8'
15
+ UTF16 = 'utf-16'
16
+
17
+
18
+ class MouseMode:
19
+ """Mouse capture settings for the winpty backend."""
20
+
21
+ # QuickEdit mode is initially disabled, and the agent does not send mouse
22
+ # mode sequences to the terminal. If it receives mouse input, though, it
23
+ # still writes MOUSE_EVENT_RECORD values into CONIN.
24
+ WINPTY_MOUSE_MODE_NONE = 0
25
+
26
+ # QuickEdit mode is initially enabled. As CONIN enters or leaves mouse
27
+ # input mode (i.e. where ENABLE_MOUSE_INPUT is on and
28
+ # ENABLE_QUICK_EDIT_MODE is off), the agent enables or disables mouse
29
+ # input on the terminal.
30
+ WINPTY_MOUSE_MODE_AUTO = 1
31
+
32
+ # QuickEdit mode is initially disabled, and the agent enables the
33
+ # terminal's mouse input mode. It does not disable terminal
34
+ # mouse mode (until exit).
35
+ WINPTY_MOUSE_MODE_FORCE = 2
36
+
37
+
38
+ class AgentConfig:
39
+ """General configuration settings for the winpty backend."""
40
+
41
+ # Create a new screen buffer (connected to the "conerr" terminal pipe) and
42
+ # pass it to child processes as the STDERR handle. This flag also prevents
43
+ # the agent from reopening CONOUT$ when it polls -- regardless of whether
44
+ # the active screen buffer changes, winpty continues to monitor the
45
+ # original primary screen buffer.
46
+ WINPTY_FLAG_CONERR = 0x1
47
+
48
+ # Don't output escape sequences.
49
+ WINPTY_FLAG_PLAIN_OUTPUT = 0x2
50
+
51
+ # Do output color escape sequences. These escapes are output by default,
52
+ # but are suppressed with WINPTY_FLAG_PLAIN_OUTPUT.
53
+ # Use this flag to re-enable them.
54
+ WINPTY_FLAG_COLOR_ESCAPES = 0x4
winpty/ptyprocess.py ADDED
@@ -0,0 +1,372 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Standard library imports
4
+ import os
5
+ import shlex
6
+ import signal
7
+ import socket
8
+ import subprocess
9
+ import threading
10
+ import time
11
+ from shutil import which
12
+
13
+ # Local imports
14
+ from .winpty import PTY
15
+
16
+
17
+ class PtyProcess(object):
18
+ """This class represents a process running in a pseudoterminal.
19
+
20
+ The main constructor is the :meth:`spawn` classmethod.
21
+ """
22
+
23
+ def __init__(self, pty):
24
+ assert isinstance(pty, PTY)
25
+ self.pty = pty
26
+ self.pid = pty.pid
27
+ # self.fd = pty.fd
28
+ self.argv = None
29
+ self.env = None
30
+ self.launch_dir = None
31
+
32
+ self.read_blocking = bool(int(os.environ.get('PYWINPTY_BLOCK', 1)))
33
+ self.closed = False
34
+ self.flag_eof = False
35
+
36
+ # Used by terminate() to give kernel time to update process status.
37
+ # Time in seconds.
38
+ self.delayafterterminate = 0.1
39
+ # Used by close() to give kernel time to update process status.
40
+ # Time in seconds.
41
+ self.delayafterclose = 0.1
42
+
43
+ # Set up our file reader sockets.
44
+ self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
45
+ self._server.bind(("127.0.0.1", 0))
46
+ address = self._server.getsockname()
47
+ self._server.listen(1)
48
+
49
+ # Read from the pty in a thread.
50
+ self._thread = threading.Thread(target=_read_in_thread,
51
+ args=(address, self.pty, self.read_blocking))
52
+ self._thread.daemon = True
53
+ self._thread.start()
54
+
55
+ self.fileobj, _ = self._server.accept()
56
+ self.fd = self.fileobj.fileno()
57
+
58
+ @classmethod
59
+ def spawn(cls, argv, cwd=None, env=None, dimensions=(24, 80),
60
+ backend=None):
61
+ """Start the given command in a child process in a pseudo terminal.
62
+
63
+ This does all the setting up the pty, and returns an instance of
64
+ PtyProcess.
65
+
66
+ Dimensions of the psuedoterminal used for the subprocess can be
67
+ specified as a tuple (rows, cols), or the default (24, 80) will be
68
+ used.
69
+ """
70
+ if isinstance(argv, str):
71
+ argv = shlex.split(argv, posix=False)
72
+
73
+ if not isinstance(argv, (list, tuple)):
74
+ raise TypeError("Expected a list or tuple for argv, got %r" % argv)
75
+
76
+ # Shallow copy of argv so we can modify it
77
+ _argv: list[str] = list(argv[:])
78
+ command = _argv[0]
79
+ env = env or os.environ
80
+
81
+ path = env.get('PATH', os.defpath)
82
+ command_with_path = which(command, path=path)
83
+ if command_with_path is None:
84
+ raise FileNotFoundError(
85
+ 'The command was not found or was not ' +
86
+ 'executable: %s.' % command
87
+ )
88
+ command = command_with_path
89
+ _argv[0] = command
90
+ cmdline = ' ' + subprocess.list2cmdline(_argv[1:])
91
+ cwd = cwd or os.getcwd()
92
+
93
+ backend = backend or os.environ.get('PYWINPTY_BACKEND', None)
94
+ backend = int(backend) if backend is not None else backend
95
+
96
+ proc = PTY(dimensions[1], dimensions[0],
97
+ backend=backend)
98
+
99
+ # Create the environment string.
100
+ envStrs = []
101
+ for (key, value) in env.items():
102
+ envStrs.append('%s=%s' % (key, value))
103
+ env = '\0'.join(envStrs) + '\0'
104
+
105
+ # command = bytes(command, encoding)
106
+ # cwd = bytes(cwd, encoding)
107
+ # cmdline = bytes(cmdline, encoding)
108
+ # env = bytes(env, encoding)
109
+
110
+ if len(_argv) == 1:
111
+ proc.spawn(command, cwd=cwd, env=env)
112
+ else:
113
+ proc.spawn(command, cwd=cwd, env=env, cmdline=cmdline)
114
+
115
+ inst = cls(proc)
116
+ inst._winsize = dimensions
117
+
118
+ # Set some informational attributes
119
+ inst.argv = _argv
120
+ if env is not None:
121
+ inst.env = env
122
+ if cwd is not None:
123
+ inst.launch_dir = cwd
124
+
125
+ return inst
126
+
127
+ @property
128
+ def exitstatus(self):
129
+ """The exit status of the process.
130
+ """
131
+ return self.pty.get_exitstatus()
132
+
133
+ def fileno(self):
134
+ """This returns the file descriptor of the pty for the child.
135
+ """
136
+ return self.fd
137
+
138
+ def close(self, force=False):
139
+ """This closes the connection with the child application. Note that
140
+ calling close() more than once is valid. This emulates standard Python
141
+ behavior with files. Set force to True if you want to make sure that
142
+ the child is terminated (SIGKILL is sent if the child ignores
143
+ SIGINT)."""
144
+ if not self.closed:
145
+ self.fileobj.close()
146
+ self._server.close()
147
+ # Give kernel time to update process status.
148
+ time.sleep(self.delayafterclose)
149
+ if self.isalive():
150
+ if not self.terminate(force):
151
+ raise IOError('Could not terminate the child.')
152
+ self.fd = -1
153
+ self.closed = True
154
+ # del self.pty
155
+
156
+ def __del__(self):
157
+ """This makes sure that no system resources are left open. Python only
158
+ garbage collects Python objects. OS file descriptors are not Python
159
+ objects, so they must be handled explicitly. If the child file
160
+ descriptor was opened outside of this class (passed to the constructor)
161
+ then this does not close it.
162
+ """
163
+ # It is possible for __del__ methods to execute during the
164
+ # teardown of the Python VM itself. Thus self.close() may
165
+ # trigger an exception because os.close may be None.
166
+ try:
167
+ self.close()
168
+ except Exception:
169
+ pass
170
+
171
+ def flush(self):
172
+ """This does nothing. It is here to support the interface for a
173
+ File-like object. """
174
+ pass
175
+
176
+ def isatty(self):
177
+ """This returns True if the file descriptor is open and connected to a
178
+ tty(-like) device, else False."""
179
+ return self.isalive()
180
+
181
+ def read(self, size=1024):
182
+ """Read and return at most ``size`` characters from the pty.
183
+
184
+ Can block if there is nothing to read. Raises :exc:`EOFError` if the
185
+ terminal was closed.
186
+ """
187
+ # try:
188
+ # data = self.pty.read(size, blocking=self.read_blocking)
189
+ # except Exception as e:
190
+ # if "EOF" in str(e):
191
+ # raise EOFError(e) from e
192
+ # return data
193
+ data = self.fileobj.recv(size)
194
+ if not data:
195
+ self.flag_eof = True
196
+ raise EOFError('Pty is closed')
197
+
198
+ if data == b'0011Ignore':
199
+ data = b''
200
+
201
+ err = True
202
+ while err and data:
203
+ try:
204
+ data.decode('utf-8')
205
+ err = False
206
+ except UnicodeDecodeError:
207
+ data += self.fileobj.recv(1)
208
+ return data.decode('utf-8')
209
+
210
+ def readline(self):
211
+ """Read one line from the pseudoterminal as bytes.
212
+
213
+ Can block if there is nothing to read. Raises :exc:`EOFError` if the
214
+ terminal was closed.
215
+ """
216
+ buf = []
217
+ while 1:
218
+ try:
219
+ ch = self.read(1)
220
+ except EOFError:
221
+ return ''.join(buf)
222
+ buf.append(ch)
223
+ if ch == '\n':
224
+ return ''.join(buf)
225
+
226
+ def write(self, s):
227
+ """Write the string ``s`` to the pseudoterminal.
228
+
229
+ Returns the number of bytes written.
230
+ """
231
+ if not self.pty.isalive():
232
+ raise EOFError('Pty is closed')
233
+
234
+ nbytes = self.pty.write(s)
235
+ return nbytes
236
+
237
+ def terminate(self, force=False):
238
+ """This forces a child process to terminate."""
239
+ if not self.isalive():
240
+ return True
241
+ self.kill(signal.SIGINT)
242
+ try:
243
+ self.pty.cancel_io()
244
+ except Exception:
245
+ pass
246
+ time.sleep(self.delayafterterminate)
247
+ if not self.isalive():
248
+ return True
249
+ if force:
250
+ self.kill(signal.SIGTERM)
251
+ time.sleep(self.delayafterterminate)
252
+ if not self.isalive():
253
+ return True
254
+ else:
255
+ return False
256
+
257
+ def wait(self):
258
+ """This waits until the child exits. This is a blocking call. This will
259
+ not read any data from the child.
260
+ """
261
+ while self.isalive():
262
+ time.sleep(0.1)
263
+ return self.exitstatus
264
+
265
+ def isalive(self):
266
+ """This tests if the child process is running or not. This is
267
+ non-blocking. If the child was terminated then this will read the
268
+ exitstatus or signalstatus of the child. This returns True if the child
269
+ process appears to be running or False if not.
270
+ """
271
+ alive = self.pty.isalive()
272
+ self.closed = not alive
273
+ return alive
274
+
275
+ def kill(self, sig):
276
+ """Kill the process with the given signal.
277
+ """
278
+ if self.pid is None:
279
+ return
280
+ os.kill(self.pid, sig)
281
+
282
+ def sendcontrol(self, char):
283
+ '''Helper method that wraps send() with mnemonic access for sending control
284
+ character to the child (such as Ctrl-C or Ctrl-D). For example, to send
285
+ Ctrl-G (ASCII 7, bell, '\a')::
286
+ child.sendcontrol('g')
287
+ See also, sendintr() and sendeof().
288
+ '''
289
+ char = char.lower()
290
+ a = ord(char)
291
+ if 97 <= a <= 122:
292
+ a = a - ord('a') + 1
293
+ byte = bytes([a]).decode("ascii")
294
+ return self.pty.write(byte), byte
295
+ d = {'@': 0, '`': 0,
296
+ '[': 27, '{': 27,
297
+ '\\': 28, '|': 28,
298
+ ']': 29, '}': 29,
299
+ '^': 30, '~': 30,
300
+ '_': 31,
301
+ '?': 127}
302
+ if char not in d:
303
+ return 0, ''
304
+
305
+ byte = bytes([d[char]]).decode("ascii")
306
+ return self.pty.write(byte), byte
307
+
308
+ def sendeof(self):
309
+ """This sends an EOF to the child. This sends a character which causes
310
+ the pending parent output buffer to be sent to the waiting child
311
+ program without waiting for end-of-line. If it is the first character
312
+ of the line, the read() in the user program returns 0, which signifies
313
+ end-of-file. This means to work as expected a sendeof() has to be
314
+ called at the beginning of a line. This method does not send a newline.
315
+ It is the responsibility of the caller to ensure the eof is sent at the
316
+ beginning of a line."""
317
+ # Send control character 4 (Ctrl-D)
318
+ self.pty.write('\x04')
319
+
320
+ def sendintr(self):
321
+ """This sends a SIGINT to the child. It does not require
322
+ the SIGINT to be the first character on a line. """
323
+ # Send control character 3 (Ctrl-C)
324
+ self.pty.write('\x03')
325
+
326
+ def eof(self):
327
+ """This returns True if the EOF exception was ever raised.
328
+ """
329
+ return self.flag_eof
330
+
331
+ def getwinsize(self):
332
+ """Return the window size of the pseudoterminal as a tuple (rows, cols).
333
+ """
334
+ return self._winsize
335
+
336
+ def setwinsize(self, rows, cols):
337
+ """Set the terminal window size of the child tty.
338
+ """
339
+ self._winsize = (rows, cols)
340
+ self.pty.set_size(cols, rows)
341
+
342
+
343
+ def _read_in_thread(address, pty: PTY, blocking: bool):
344
+ """Read data from the pty in a thread.
345
+ """
346
+ client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
347
+ client.connect(address)
348
+
349
+ call = 0
350
+
351
+ while 1:
352
+ try:
353
+ data = pty.read(blocking=blocking) or '0011Ignore'
354
+ try:
355
+ client.send(bytes(data, 'utf-8'))
356
+ except socket.error:
357
+ break
358
+
359
+ # Handle end of file.
360
+ if pty.iseof():
361
+ try:
362
+ client.send(b'')
363
+ except socket.error:
364
+ pass
365
+ break
366
+
367
+ call += 1
368
+ except Exception as e:
369
+ break
370
+ time.sleep(1e-3)
371
+
372
+ client.close()
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """winpty module tests."""
@@ -0,0 +1,164 @@
1
+ # -*- coding: utf-8 -*-
2
+ """winpty wrapper tests."""
3
+
4
+ # Standard library imports
5
+ import os
6
+ import time
7
+
8
+ # Third party imports
9
+ from winpty import PTY, WinptyError
10
+ from winpty.enums import Backend
11
+ from winpty.ptyprocess import which
12
+ import pytest
13
+
14
+
15
+ CMD = which('cmd').lower()
16
+
17
+
18
+ def pty_factory(backend):
19
+ if os.environ.get('CI_RUNNING', None) == '1':
20
+ if backend == Backend.ConPTY:
21
+ os.environ['CONPTY_CI'] = '1'
22
+ elif backend == Backend.WinPTY:
23
+ os.environ.pop('CONPTY_CI', None)
24
+
25
+ @pytest.fixture(scope='function')
26
+ def pty_fixture():
27
+ pty = PTY(80, 20, backend=backend)
28
+ # loc = bytes(os.getcwd(), 'utf8')
29
+ assert pty.spawn(CMD)
30
+ time.sleep(0.3)
31
+ # if backend == Backend.ConPTY:
32
+ # pty.write("\x1b[?1;0c\x1b[0;0R")
33
+ return pty
34
+ return pty_fixture
35
+
36
+
37
+ conpty_provider = pty_factory(Backend.ConPTY)
38
+ winpty_provider = pty_factory(Backend.WinPTY)
39
+
40
+
41
+ @pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
42
+ def pty_fixture(request):
43
+ backend = request.param
44
+ if os.environ.get('CI_RUNNING', None) == '1':
45
+ if backend == 'ConPTY':
46
+ os.environ['CI'] = '1'
47
+ os.environ['CONPTY_CI'] = '1'
48
+ if backend == 'WinPTY':
49
+ os.environ.pop('CI', None)
50
+ os.environ.pop('CONPTY_CI', None)
51
+
52
+ backend = getattr(Backend, backend)
53
+ def _pty_factory():
54
+ try:
55
+ pty = PTY(80, 25, backend=backend)
56
+ except WinptyError:
57
+ pytest.skip()
58
+ return None
59
+ assert pty.spawn(CMD)
60
+ time.sleep(0.3)
61
+ return pty
62
+ return _pty_factory
63
+
64
+
65
+ # @pytest.fixture(scope='function', params=[
66
+ # pytest.lazy_fixture('conpty_provider'),
67
+ # pytest.lazy_fixture('winpty_provider')])
68
+ # def pty_fixture(request):
69
+ # pty = request.param
70
+ # return pty
71
+
72
+
73
+ def test_read(pty_fixture, capsys):
74
+ pty = pty_fixture()
75
+ loc = os.getcwd()
76
+ readline = ''
77
+
78
+ with capsys.disabled():
79
+ start_time = time.time()
80
+ while loc not in readline:
81
+ if time.time() - start_time > 5:
82
+ break
83
+ readline += pty.read()
84
+ assert loc in readline or 'cmd' in readline
85
+ del pty
86
+
87
+
88
+ def test_write(pty_fixture):
89
+ pty = pty_fixture()
90
+ line = pty.read()
91
+
92
+ str_text = 'Eggs, ham and spam ünicode'
93
+ # text = bytes(str_text, 'utf-8')
94
+ num_bytes = pty.write(str_text)
95
+
96
+ line = ''
97
+ start_time = time.time()
98
+ while str_text not in line:
99
+ if time.time() - start_time > 5:
100
+ break
101
+ line += pty.read()
102
+
103
+ assert str_text in line
104
+ del pty
105
+
106
+
107
+ def test_isalive(pty_fixture):
108
+ pty = pty_fixture()
109
+ pty.write('exit\r\n')
110
+
111
+ text = 'exit'
112
+ line = ''
113
+ while text not in line:
114
+ try:
115
+ line += pty.read()
116
+ except Exception:
117
+ break
118
+
119
+ while pty.isalive():
120
+ try:
121
+ pty.read()
122
+ # continue
123
+ except Exception:
124
+ break
125
+
126
+ assert not pty.isalive()
127
+ del pty
128
+
129
+
130
+ # def test_agent_spawn_fail(pty_fixture):
131
+ # pty = pty_fixture
132
+ # try:
133
+ # pty.spawn(CMD)
134
+ # assert False
135
+ # except WinptyError:
136
+ # pass
137
+
138
+
139
+ # @pytest.mark.parametrize(
140
+ # 'backend_name,backend',
141
+ # [("ConPTY", Backend.ConPTY), ('WinPTY', Backend.WinPTY)])
142
+ # def test_pty_create_size_fail(backend_name, backend):
143
+ # try:
144
+ # PTY(80, -25, backend=backend)
145
+ # assert False
146
+ # except WinptyError:
147
+ # pass
148
+
149
+
150
+ # def test_agent_resize_fail(pty_fixture):
151
+ # pty = pty_fixture()
152
+ # try:
153
+ # pty.set_size(-80, 70)
154
+ # assert False
155
+ # except WinptyError:
156
+ # pass
157
+ # finally:
158
+ # del pty
159
+
160
+
161
+ def test_agent_resize(pty_fixture):
162
+ pty = pty_fixture()
163
+ pty.set_size(80, 70)
164
+ del pty
@@ -0,0 +1,268 @@
1
+ # -*- coding: utf-8 -*-
2
+ """winpty wrapper tests."""
3
+
4
+ # Standard library imports
5
+ import asyncio
6
+ import os
7
+ import signal
8
+ import time
9
+ import sys
10
+ import re
11
+
12
+ # Third party imports
13
+ import pytest
14
+ from flaky import flaky
15
+
16
+ # Local imports
17
+ from winpty import WinptyError
18
+ from winpty.enums import Backend
19
+ from winpty.ptyprocess import PtyProcess, which
20
+
21
+
22
+
23
+ @pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
24
+ def pty_fixture(request):
25
+ backend = request.param
26
+ if os.environ.get('CI_RUNNING', None) == '1':
27
+ if backend == 'ConPTY':
28
+ os.environ['CI'] = '1'
29
+ os.environ['CONPTY_CI'] = '1'
30
+ if backend == 'WinPTY':
31
+ os.environ.pop('CI', None)
32
+ os.environ.pop('CONPTY_CI', None)
33
+
34
+ backend = getattr(Backend, backend)
35
+ def _pty_factory(cmd=None, env=None):
36
+ cmd = cmd or 'cmd'
37
+ try:
38
+ pty = PtyProcess.spawn(cmd, env=env, backend=backend)
39
+ except WinptyError:
40
+ pytest.skip()
41
+ return None
42
+ return pty
43
+ # time.sleep(10)
44
+ _pty_factory.backend = request.param
45
+ return _pty_factory
46
+
47
+
48
+ @flaky(max_runs=40, min_passes=1)
49
+ def test_read(pty_fixture):
50
+ pty = pty_fixture()
51
+ loc = os.getcwd()
52
+ data = ''
53
+ tries = 0
54
+ while loc not in data and tries < 10:
55
+ try:
56
+ data += pty.read()
57
+ except EOFError:
58
+ pass
59
+ tries += 1
60
+ assert loc in data
61
+ pty.terminate()
62
+ time.sleep(2)
63
+
64
+
65
+ @flaky(max_runs=40, min_passes=1)
66
+ def test_write(pty_fixture):
67
+ pty = pty_fixture()
68
+
69
+ text = 'Eggs, ham and spam ünicode'
70
+ pty.write(text)
71
+
72
+ data = ''
73
+ tries = 0
74
+ while text not in data and tries < 10:
75
+ try:
76
+ data += pty.read()
77
+ except EOFError:
78
+ pass
79
+ tries += 1
80
+ assert text in data
81
+ pty.terminate()
82
+
83
+
84
+ @pytest.mark.xfail(reason="It fails sometimes due to long strings")
85
+ @flaky(max_runs=40, min_passes=1)
86
+ def test_isalive(pty_fixture):
87
+ pty = pty_fixture()
88
+ time.sleep(4)
89
+
90
+ pty.write('echo \"foo\"\r\nexit\r\n')
91
+ data = ''
92
+ while True:
93
+ try:
94
+ print('Stuck')
95
+ data += pty.read()
96
+ except EOFError:
97
+ break
98
+
99
+ regex = re.compile(".*foo.*")
100
+ assert regex.findall(data)
101
+ assert not pty.isalive()
102
+ pty.terminate()
103
+
104
+
105
+ @pytest.mark.xfail(reason="It fails sometimes due to long strings")
106
+ @flaky(max_runs=40, min_passes=1)
107
+ def test_readline(pty_fixture):
108
+ env = os.environ.copy()
109
+ env['foo'] = 'bar'
110
+ pty = pty_fixture(env=env)
111
+
112
+ # Ensure that the echo print has its own CRLF
113
+ pty.write('cls\r\n')
114
+ pty.write('echo %foo%\r\n')
115
+
116
+ data = ''
117
+ tries = 0
118
+ while 'bar' not in data and tries < 10:
119
+ data = pty.readline()
120
+ tries += 1
121
+
122
+ assert 'bar' in data
123
+
124
+ pty.terminate()
125
+
126
+
127
+ def test_close(pty_fixture):
128
+ pty = pty_fixture()
129
+ pty.close()
130
+ assert not pty.isalive()
131
+
132
+
133
+ def test_flush(pty_fixture):
134
+ pty = pty_fixture()
135
+ pty.flush()
136
+ pty.terminate()
137
+
138
+
139
+ def test_intr(pty_fixture):
140
+ pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
141
+ pty.sendintr()
142
+ assert pty.wait() != 0
143
+
144
+
145
+ def test_send_control(pty_fixture):
146
+ pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
147
+ pty.sendcontrol('d')
148
+ assert pty.wait() != 0
149
+
150
+
151
+ @pytest.mark.skipif(which('cat') is None, reason="Requires cat on the PATH")
152
+ def test_send_eof(pty_fixture):
153
+ cat = pty_fixture('cat')
154
+ cat.sendeof()
155
+ assert cat.wait() == 0
156
+
157
+
158
+ def test_isatty(pty_fixture):
159
+ pty = pty_fixture()
160
+ assert pty.isatty()
161
+ pty.terminate()
162
+ assert not pty.isatty()
163
+
164
+
165
+ def test_wait(pty_fixture):
166
+ pty = pty_fixture(cmd=[sys.executable, '--version'])
167
+ assert pty.wait() == 0
168
+
169
+
170
+ def test_exit_status(pty_fixture):
171
+ pty = pty_fixture(cmd=[sys.executable])
172
+ pty.write('import sys;sys.exit(1)\r\n')
173
+ pty.wait()
174
+ assert pty.exitstatus == 1
175
+
176
+
177
+ # @pytest.mark.timeout(30)
178
+ def test_kill_sigterm(pty_fixture):
179
+ pty = pty_fixture()
180
+ pty.write('echo \"foo\"\r\nsleep 1000\r\n')
181
+ pty.read()
182
+ pty.kill(signal.SIGTERM)
183
+
184
+ while True:
185
+ try:
186
+ pty.read()
187
+ except EOFError:
188
+ break
189
+
190
+ assert not pty.isalive()
191
+ assert pty.exitstatus == signal.SIGTERM
192
+
193
+
194
+ # @pytest.mark.timeout(30)
195
+ def test_terminate(pty_fixture):
196
+ pty = pty_fixture()
197
+ pty.write('echo \"foo\"\r\nsleep 1000\r\n')
198
+ pty.read()
199
+ pty.terminate()
200
+
201
+ while True:
202
+ try:
203
+ pty.read()
204
+ except EOFError:
205
+ break
206
+
207
+ assert not pty.isalive()
208
+ assert pty.closed
209
+
210
+
211
+ # @pytest.mark.timeout(30)
212
+ def test_terminate_force(pty_fixture):
213
+ pty = pty_fixture()
214
+ pty.write('echo \"foo\"\r\nsleep 1000\r\n')
215
+ pty.read()
216
+ pty.terminate(force=True)
217
+
218
+ while True:
219
+ try:
220
+ pty.read()
221
+ except EOFError:
222
+ break
223
+
224
+ assert not pty.isalive()
225
+ assert pty.closed
226
+
227
+
228
+ def test_terminate_loop(pty_fixture):
229
+ pty = pty_fixture()
230
+ loop = asyncio.SelectorEventLoop()
231
+ asyncio.set_event_loop(loop)
232
+
233
+ def reader():
234
+ try:
235
+ data = pty.read()
236
+ except EOFError:
237
+ loop.remove_reader(pty.fd)
238
+ loop.stop()
239
+
240
+ loop.add_reader(pty.fd, reader)
241
+ loop.call_soon(pty.write, 'echo \"foo\"\r\nsleep 1000\r\n')
242
+ loop.call_soon(pty.terminate, True)
243
+
244
+ try:
245
+ loop.run_forever()
246
+ finally:
247
+ loop.close()
248
+
249
+ assert not pty.isalive()
250
+ assert pty.closed
251
+
252
+
253
+ def test_getwinsize(pty_fixture):
254
+ pty = pty_fixture()
255
+ assert pty.getwinsize() == (24, 80)
256
+ pty.terminate()
257
+
258
+
259
+ def test_setwinsize(pty_fixture):
260
+ pty = pty_fixture()
261
+ pty.setwinsize(50, 110)
262
+ assert pty.getwinsize() == (50, 110)
263
+ pty.terminate()
264
+
265
+ pty = PtyProcess.spawn('cmd', dimensions=(60, 120))
266
+ assert pty.getwinsize() == (60, 120)
267
+ pty.terminate()
268
+
Binary file
winpty/winpty.pyi ADDED
@@ -0,0 +1,42 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Stub typing declarations for the native PTY object."""
4
+
5
+ # Standard library imports
6
+ from typing import Optional
7
+
8
+ # Local imports
9
+ from .enums import MouseMode, AgentConfig
10
+
11
+ __version__: str
12
+
13
+ class WinptyError(Exception): ...
14
+
15
+ class PTY:
16
+ def __init__(
17
+ self,
18
+ cols: int,
19
+ rows: int,
20
+ backend: Optional[int] = None,
21
+ mouse_mode: int = MouseMode.WINPTY_MOUSE_MODE_NONE,
22
+ timeout: int = 30000,
23
+ agent_config: int = AgentConfig.WINPTY_FLAG_COLOR_ESCAPES,
24
+ ): ...
25
+ def spawn(
26
+ self,
27
+ appname: str,
28
+ cmdline: Optional[str] = None,
29
+ cwd: Optional[str] = None,
30
+ env: Optional[str] = None,
31
+ ) -> bool: ...
32
+ def set_size(self, cols: int, rows: int): ...
33
+ def read(self, blocking: bool = False) -> str: ...
34
+ def write(self, to_write: str) -> int: ...
35
+ def isalive(self) -> bool: ...
36
+ def get_exitstatus(self) -> Optional[int]: ...
37
+ def iseof(self) -> bool: ...
38
+ def cancel_io(self) -> bool: ...
39
+ @property
40
+ def pid(self) -> Optional[int]: ...
41
+ @property
42
+ def fd(self) -> Optional[int]: ...