pywinpty 2.0.15__cp313-cp313t-win_amd64.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,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: pywinpty
3
+ Version: 2.0.15
4
+ License-File: LICENSE.txt
5
+ Summary: Pseudo terminal support for Windows from Python.
6
+ Keywords: pty,pseudo-terminal,conpty,windows,winpty
7
+ Author: Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
8
+ Author-email: Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
9
+ License: MIT
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
12
+ Project-URL: Source Code, https://github.com/spyder-ide/pywinpty
13
+
14
+ # PyWinpty: Pseudoterminals for Windows in Python
15
+
16
+ [![Project License - MIT](https://img.shields.io/pypi/l/pywinpty.svg)](./LICENSE.txt)
17
+ [![pypi version](https://img.shields.io/pypi/v/pywinpty.svg)](https://pypi.org/project/pywinpty/)
18
+ [![conda version](https://img.shields.io/conda/vn/conda-forge/pywinpty.svg)](https://www.anaconda.com/download/)
19
+ [![download count](https://img.shields.io/conda/dn/conda-forge/pywinpty.svg)](https://www.anaconda.com/download/)
20
+ [![Downloads](https://pepy.tech/badge/pywinpty)](https://pepy.tech/project/pywinpty)
21
+ [![PyPI status](https://img.shields.io/pypi/status/pywinpty.svg)](https://github.com/spyder-ide/pywinpty)
22
+ [![Windows tests](https://github.com/andfoy/pywinpty/actions/workflows/windows_build.yml/badge.svg)](https://github.com/andfoy/pywinpty/actions/workflows/windows_build.yml)
23
+
24
+ *Copyright © 2017–2022 Spyder Project Contributors*
25
+ *Copyright © 2022– Edgar Andrés Margffoy Tuay*
26
+
27
+
28
+ ## Overview
29
+
30
+ 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.
31
+
32
+
33
+ ## Dependencies
34
+ To compile pywinpty sources, you must have [Rust](https://rustup.rs/) installed.
35
+ Optionally, you can also have Winpty's C header and library files available on your include path.
36
+
37
+
38
+ ## Installation
39
+ You can install this library by using conda or pip package managers, as it follows:
40
+
41
+ Using conda (Recommended):
42
+ ```bash
43
+ conda install pywinpty
44
+ ```
45
+
46
+ Using pip:
47
+ ```bash
48
+ pip install pywinpty
49
+ ```
50
+
51
+ ## Building from source
52
+
53
+ To build from sources, you will require both a working stable or nightly Rust toolchain with
54
+ target `x86_64-pc-windows-msvc`, which can be installed using [rustup](https://rustup.rs/).
55
+
56
+ Optionally, this library can be linked against winpty library, which you can install using conda-forge:
57
+
58
+ ```batch
59
+ conda install winpty -c conda-forge
60
+ ```
61
+
62
+ If you don't want to use conda, you will need to have the winpty binaries and headers available on your PATH.
63
+
64
+ Finally, pywinpty uses [Maturin](https://github.com/PyO3/maturin) as the build backend, which can be installed using `pip`:
65
+
66
+ ```batch
67
+ pip install maturin
68
+ ```
69
+
70
+ To test your compilation environment settings, you can build pywinpty sources locally, by
71
+ executing:
72
+
73
+ ```bash
74
+ maturin develop
75
+ ```
76
+
77
+ This package depends on the following Rust crates:
78
+
79
+ * [PyO3](https://github.com/PyO3/pyo3): Library used to produce Python bindings from Rust code.
80
+ * [WinPTY-rs](https://github.com/andfoy/winpty-rs): Create and spawn processes inside a pseudoterminal in Windows from Rust.
81
+ * [Maturin](https://github.com/PyO3/maturin): Build system to build and publish Rust-based Python packages.
82
+
83
+ ## Package usage
84
+ Pywinpty offers a single python wrapper around winpty library functions.
85
+ This implies that using a single object (``winpty.PTY``) it is possible to access to all functionality, as it follows:
86
+
87
+ ```python
88
+ # High level usage using `spawn`
89
+ from winpty import PtyProcess
90
+
91
+ proc = PtyProcess.spawn('python')
92
+ proc.write('print("hello, world!")\r\n')
93
+ proc.write('exit()\r\n')
94
+ while proc.isalive():
95
+ print(proc.readline())
96
+
97
+ # Low level usage using the raw `PTY` object
98
+ from winpty import PTY
99
+
100
+ # Start a new winpty-agent process of size (cols, rows)
101
+ cols, rows = 80, 25
102
+ process = PTY(cols, rows)
103
+
104
+ # Spawn a new console process, e.g., CMD
105
+ process.spawn(br'C:\windows\system32\cmd.exe')
106
+
107
+ # Read console output (Unicode)
108
+ process.read()
109
+
110
+ # Write input to console (Unicode)
111
+ process.write(b'Text')
112
+
113
+ # Resize console size
114
+ new_cols, new_rows = 90, 30
115
+ process.set_size(new_cols, new_rows)
116
+
117
+ # Know if the process is alive
118
+ alive = process.isalive()
119
+
120
+ # End winpty-agent process
121
+ del process
122
+ ```
123
+
124
+ ## Running tests
125
+ We use pytest to run tests as it follows (after calling ``maturin develop``), the test suite depends
126
+ on pytest-lazy-fixture, which can be installed via pip:
127
+
128
+ ```batch
129
+ pip install pytest pytest-lazy-fixture flaky
130
+ ```
131
+
132
+ All the tests can be exceuted using the following command
133
+
134
+ ```bash
135
+ python runtests.py
136
+ ```
137
+
138
+
139
+ ## Changelog
140
+ Visit our [CHANGELOG](CHANGELOG.md) file to learn more about our new features and improvements.
141
+
142
+
143
+ ## Contribution guidelines
144
+ 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.
145
+
@@ -0,0 +1,14 @@
1
+ pywinpty-2.0.15.dist-info/METADATA,sha256=JvigkiTz4bbGOWiYRrvpK42n6ASsNzKxr5j3LPpQl-o,5195
2
+ pywinpty-2.0.15.dist-info/WHEEL,sha256=oRu0fsH4I6bFBiGWNtG0KH0znJBs56H_sBgQgZoml3o,97
3
+ pywinpty-2.0.15.dist-info/licenses/LICENSE.txt,sha256=-HjUdn-a0uQ9MIPvoAIBsADOk32e6GJuALpccqrJUeI,1088
4
+ winpty/enums.py,sha256=fhHvBfb0ciVR2XsAiBn67Q-zNDlUGvakqlyZ2XRicig,1859
5
+ winpty/ptyprocess.py,sha256=OIrWzipZZ5zZSqc7k__megCZ9yWkepSzAP04Du0G2H4,12021
6
+ winpty/tests/test_pty.py,sha256=PqZPzzZqfdABi_eW0KFVkDdSd8uskPtUKHj0Lw7uPIA,3725
7
+ winpty/tests/test_ptyprocess.py,sha256=F-5Tp8nQnPhpJlTEKJTM3XDH-VVEbcYr7DsKiJMHT2g,5915
8
+ winpty/tests/__init__.py,sha256=fzb9cDnPt2R3b_rWh6sqDgIbiQOZtBlfsV1aq-ULT2Q,53
9
+ winpty/winpty-agent.exe,sha256=REZ6g9hrYSe8e4duiy-zP6df3gnZuf8nx_jvhLqWmyk,2627338
10
+ winpty/winpty.dll,sha256=UXfzIarC-oVDbdbsa_xtQUWdJfW1M6oDNJ71E0YbTHk,2509089
11
+ winpty/winpty.pyi,sha256=CAFZIWyBAGwV-2pekJUF5ft6uxRwFo-5n0UGEbiToRE,1418
12
+ winpty/__init__.py,sha256=laTi9sLjCLycyaQYO3wMvhZNIK2vh_pOvGTJT2Od4Qs,402
13
+ winpty/winpty.cp313t-win_amd64.pyd,sha256=U9E4Adk-S17-wJwlcuMV_AnjHhf57WtfjXmVKDDiCP4,531456
14
+ pywinpty-2.0.15.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.8.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313t-win_amd64
@@ -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 reenable them.
54
+ WINPTY_FLAG_COLOR_ESCAPES = 0x4
winpty/ptyprocess.py ADDED
@@ -0,0 +1,365 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Standard library imports
4
+ import codecs
5
+ import os
6
+ import shlex
7
+ import signal
8
+ import socket
9
+ import subprocess
10
+ import threading
11
+ import time
12
+ from shutil import which
13
+
14
+ # Local imports
15
+ from .winpty import PTY
16
+
17
+
18
+ class PtyProcess(object):
19
+ """This class represents a process running in a pseudoterminal.
20
+
21
+ The main constructor is the :meth:`spawn` classmethod.
22
+ """
23
+
24
+ def __init__(self, pty):
25
+ assert isinstance(pty, PTY)
26
+ self.pty = pty
27
+ self.pid = pty.pid
28
+ # self.fd = pty.fd
29
+
30
+ self.read_blocking = bool(int(os.environ.get('PYWINPTY_BLOCK', 1)))
31
+ self.closed = False
32
+ self.flag_eof = False
33
+
34
+ # Used by terminate() to give kernel time to update process status.
35
+ # Time in seconds.
36
+ self.delayafterterminate = 0.1
37
+ # Used by close() to give kernel time to update process status.
38
+ # Time in seconds.
39
+ self.delayafterclose = 0.1
40
+
41
+ # Set up our file reader sockets.
42
+ self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
43
+ self._server.bind(("127.0.0.1", 0))
44
+ address = self._server.getsockname()
45
+ self._server.listen(1)
46
+
47
+ # Read from the pty in a thread.
48
+ self._thread = threading.Thread(target=_read_in_thread,
49
+ args=(address, self.pty, self.read_blocking))
50
+ self._thread.daemon = True
51
+ self._thread.start()
52
+
53
+ self.fileobj, _ = self._server.accept()
54
+ self.fd = self.fileobj.fileno()
55
+
56
+ @classmethod
57
+ def spawn(cls, argv, cwd=None, env=None, dimensions=(24, 80),
58
+ backend=None):
59
+ """Start the given command in a child process in a pseudo terminal.
60
+
61
+ This does all the setting up the pty, and returns an instance of
62
+ PtyProcess.
63
+
64
+ Dimensions of the psuedoterminal used for the subprocess can be
65
+ specified as a tuple (rows, cols), or the default (24, 80) will be
66
+ used.
67
+ """
68
+ if isinstance(argv, str):
69
+ argv = shlex.split(argv, posix=False)
70
+
71
+ if not isinstance(argv, (list, tuple)):
72
+ raise TypeError("Expected a list or tuple for argv, got %r" % argv)
73
+
74
+ # Shallow copy of argv so we can modify it
75
+ argv = argv[:]
76
+ command = argv[0]
77
+ env = env or os.environ
78
+
79
+ path = env.get('PATH', os.defpath)
80
+ command_with_path = which(command, path=path)
81
+ if command_with_path is None:
82
+ raise FileNotFoundError(
83
+ 'The command was not found or was not ' +
84
+ 'executable: %s.' % command
85
+ )
86
+ command = command_with_path
87
+ argv[0] = command
88
+ cmdline = ' ' + subprocess.list2cmdline(argv[1:])
89
+ cwd = cwd or os.getcwd()
90
+
91
+ backend = backend or os.environ.get('PYWINPTY_BACKEND', None)
92
+ backend = int(backend) if backend is not None else backend
93
+
94
+ proc = PTY(dimensions[1], dimensions[0],
95
+ backend=backend)
96
+
97
+ # Create the environemnt string.
98
+ envStrs = []
99
+ for (key, value) in env.items():
100
+ envStrs.append('%s=%s' % (key, value))
101
+ env = '\0'.join(envStrs) + '\0'
102
+
103
+ # command = bytes(command, encoding)
104
+ # cwd = bytes(cwd, encoding)
105
+ # cmdline = bytes(cmdline, encoding)
106
+ # env = bytes(env, encoding)
107
+
108
+ if len(argv) == 1:
109
+ proc.spawn(command, cwd=cwd, env=env)
110
+ else:
111
+ proc.spawn(command, cwd=cwd, env=env, cmdline=cmdline)
112
+
113
+ inst = cls(proc)
114
+ inst._winsize = dimensions
115
+
116
+ # Set some informational attributes
117
+ inst.argv = argv
118
+ if env is not None:
119
+ inst.env = env
120
+ if cwd is not None:
121
+ inst.launch_dir = cwd
122
+
123
+ return inst
124
+
125
+ @property
126
+ def exitstatus(self):
127
+ """The exit status of the process.
128
+ """
129
+ return self.pty.get_exitstatus()
130
+
131
+ def fileno(self):
132
+ """This returns the file descriptor of the pty for the child.
133
+ """
134
+ return self.fd
135
+
136
+ def close(self, force=False):
137
+ """This closes the connection with the child application. Note that
138
+ calling close() more than once is valid. This emulates standard Python
139
+ behavior with files. Set force to True if you want to make sure that
140
+ the child is terminated (SIGKILL is sent if the child ignores
141
+ SIGINT)."""
142
+ if not self.closed:
143
+ self.fileobj.close()
144
+ self._server.close()
145
+ # Give kernel time to update process status.
146
+ time.sleep(self.delayafterclose)
147
+ if self.isalive():
148
+ if not self.terminate(force):
149
+ raise IOError('Could not terminate the child.')
150
+ self.fd = -1
151
+ self.closed = True
152
+ # del self.pty
153
+
154
+ def __del__(self):
155
+ """This makes sure that no system resources are left open. Python only
156
+ garbage collects Python objects. OS file descriptors are not Python
157
+ objects, so they must be handled explicitly. If the child file
158
+ descriptor was opened outside of this class (passed to the constructor)
159
+ then this does not close it.
160
+ """
161
+ # It is possible for __del__ methods to execute during the
162
+ # teardown of the Python VM itself. Thus self.close() may
163
+ # trigger an exception because os.close may be None.
164
+ try:
165
+ self.close()
166
+ except Exception:
167
+ pass
168
+
169
+ def flush(self):
170
+ """This does nothing. It is here to support the interface for a
171
+ File-like object. """
172
+ pass
173
+
174
+ def isatty(self):
175
+ """This returns True if the file descriptor is open and connected to a
176
+ tty(-like) device, else False."""
177
+ return self.isalive()
178
+
179
+ def read(self, size=1024):
180
+ """Read and return at most ``size`` characters from the pty.
181
+
182
+ Can block if there is nothing to read. Raises :exc:`EOFError` if the
183
+ terminal was closed.
184
+ """
185
+ # try:
186
+ # data = self.pty.read(size, blocking=self.read_blocking)
187
+ # except Exception as e:
188
+ # if "EOF" in str(e):
189
+ # raise EOFError(e) from e
190
+ # return data
191
+ data = self.fileobj.recv(size)
192
+ if not data:
193
+ self.flag_eof = True
194
+ raise EOFError('Pty is closed')
195
+
196
+ if data == b'0011Ignore':
197
+ data = ''
198
+
199
+ err = True
200
+ while err and data:
201
+ try:
202
+ data.decode('utf-8')
203
+ err = False
204
+ except UnicodeDecodeError:
205
+ data += self.fileobj.recv(1)
206
+ return data.decode('utf-8')
207
+
208
+ def readline(self):
209
+ """Read one line from the pseudoterminal as bytes.
210
+
211
+ Can block if there is nothing to read. Raises :exc:`EOFError` if the
212
+ terminal was closed.
213
+ """
214
+ buf = []
215
+ while 1:
216
+ try:
217
+ ch = self.read(1)
218
+ except EOFError:
219
+ return ''.join(buf)
220
+ buf.append(ch)
221
+ if ch == '\n':
222
+ return ''.join(buf)
223
+
224
+ def write(self, s):
225
+ """Write the string ``s`` to the pseudoterminal.
226
+
227
+ Returns the number of bytes written.
228
+ """
229
+ if not self.pty.isalive():
230
+ raise EOFError('Pty is closed')
231
+
232
+ nbytes = self.pty.write(s)
233
+ return nbytes
234
+
235
+ def terminate(self, force=False):
236
+ """This forces a child process to terminate."""
237
+ if not self.isalive():
238
+ return True
239
+ self.kill(signal.SIGINT)
240
+ time.sleep(self.delayafterterminate)
241
+ if not self.isalive():
242
+ return True
243
+ if force:
244
+ self.kill(signal.SIGTERM)
245
+ time.sleep(self.delayafterterminate)
246
+ if not self.isalive():
247
+ return True
248
+ else:
249
+ return False
250
+
251
+ def wait(self):
252
+ """This waits until the child exits. This is a blocking call. This will
253
+ not read any data from the child.
254
+ """
255
+ while self.isalive():
256
+ time.sleep(0.1)
257
+ return self.exitstatus
258
+
259
+ def isalive(self):
260
+ """This tests if the child process is running or not. This is
261
+ non-blocking. If the child was terminated then this will read the
262
+ exitstatus or signalstatus of the child. This returns True if the child
263
+ process appears to be running or False if not.
264
+ """
265
+ alive = self.pty.isalive()
266
+ self.closed = not alive
267
+ return alive
268
+
269
+ def kill(self, sig=None):
270
+ """Kill the process with the given signal.
271
+ """
272
+ os.kill(self.pid, sig)
273
+
274
+ def sendcontrol(self, char):
275
+ '''Helper method that wraps send() with mnemonic access for sending control
276
+ character to the child (such as Ctrl-C or Ctrl-D). For example, to send
277
+ Ctrl-G (ASCII 7, bell, '\a')::
278
+ child.sendcontrol('g')
279
+ See also, sendintr() and sendeof().
280
+ '''
281
+ char = char.lower()
282
+ a = ord(char)
283
+ if 97 <= a <= 122:
284
+ a = a - ord('a') + 1
285
+ byte = bytes([a]).decode("ascii")
286
+ return self.pty.write(byte), byte
287
+ d = {'@': 0, '`': 0,
288
+ '[': 27, '{': 27,
289
+ '\\': 28, '|': 28,
290
+ ']': 29, '}': 29,
291
+ '^': 30, '~': 30,
292
+ '_': 31,
293
+ '?': 127}
294
+ if char not in d:
295
+ return 0, ''
296
+
297
+ byte = bytes([d[char]]).decode("ascii")
298
+ return self.pty.write(byte), byte
299
+
300
+ def sendeof(self):
301
+ """This sends an EOF to the child. This sends a character which causes
302
+ the pending parent output buffer to be sent to the waiting child
303
+ program without waiting for end-of-line. If it is the first character
304
+ of the line, the read() in the user program returns 0, which signifies
305
+ end-of-file. This means to work as expected a sendeof() has to be
306
+ called at the beginning of a line. This method does not send a newline.
307
+ It is the responsibility of the caller to ensure the eof is sent at the
308
+ beginning of a line."""
309
+ # Send control character 4 (Ctrl-D)
310
+ self.pty.write('\x04'), '\x04'
311
+
312
+ def sendintr(self):
313
+ """This sends a SIGINT to the child. It does not require
314
+ the SIGINT to be the first character on a line. """
315
+ # Send control character 3 (Ctrl-C)
316
+ self.pty.write('\x03'), '\x03'
317
+
318
+ def eof(self):
319
+ """This returns True if the EOF exception was ever raised.
320
+ """
321
+ return self.flag_eof
322
+
323
+ def getwinsize(self):
324
+ """Return the window size of the pseudoterminal as a tuple (rows, cols).
325
+ """
326
+ return self._winsize
327
+
328
+ def setwinsize(self, rows, cols):
329
+ """Set the terminal window size of the child tty.
330
+ """
331
+ self._winsize = (rows, cols)
332
+ self.pty.set_size(cols, rows)
333
+
334
+
335
+ def _read_in_thread(address, pty, blocking):
336
+ """Read data from the pty in a thread.
337
+ """
338
+ client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
339
+ client.connect(address)
340
+
341
+ call = 0
342
+
343
+ while 1:
344
+ try:
345
+ data = pty.read(4096, blocking=blocking) or b'0011Ignore'
346
+ try:
347
+ client.send(bytes(data, 'utf-8'))
348
+ except socket.error:
349
+ break
350
+
351
+ # Handle end of file.
352
+ if pty.iseof():
353
+ try:
354
+ client.send(b'')
355
+ except socket.error:
356
+ pass
357
+ finally:
358
+ break
359
+
360
+ call += 1
361
+ except Exception as e:
362
+ break
363
+ time.sleep(1e-3)
364
+
365
+ client.close()
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ """winpty module tests."""
@@ -0,0 +1,159 @@
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
+ return pty
32
+ return pty_fixture
33
+
34
+
35
+ conpty_provider = pty_factory(Backend.ConPTY)
36
+ winpty_provider = pty_factory(Backend.WinPTY)
37
+
38
+
39
+ @pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
40
+ def pty_fixture(request):
41
+ backend = request.param
42
+ if os.environ.get('CI_RUNNING', None) == '1':
43
+ if backend == 'ConPTY':
44
+ os.environ['CI'] = '1'
45
+ os.environ['CONPTY_CI'] = '1'
46
+ if backend == 'WinPTY':
47
+ os.environ.pop('CI', None)
48
+ os.environ.pop('CONPTY_CI', None)
49
+
50
+ backend = getattr(Backend, backend)
51
+ def _pty_factory():
52
+ pty = PTY(80, 25, backend=backend)
53
+ assert pty.spawn(CMD)
54
+ time.sleep(0.3)
55
+ return pty
56
+ return _pty_factory
57
+
58
+
59
+
60
+ # @pytest.fixture(scope='function', params=[
61
+ # pytest.lazy_fixture('conpty_provider'),
62
+ # pytest.lazy_fixture('winpty_provider')])
63
+ # def pty_fixture(request):
64
+ # pty = request.param
65
+ # return pty
66
+
67
+
68
+ def test_read(pty_fixture, capsys):
69
+ pty = pty_fixture()
70
+ loc = os.getcwd()
71
+ readline = ''
72
+
73
+ with capsys.disabled():
74
+ start_time = time.time()
75
+ while loc not in readline:
76
+ if time.time() - start_time > 5:
77
+ break
78
+ readline += pty.read()
79
+ assert loc in readline or 'cmd' in readline
80
+ del pty
81
+
82
+
83
+ def test_write(pty_fixture):
84
+ pty = pty_fixture()
85
+ line = pty.read()
86
+
87
+ str_text = 'Eggs, ham and spam ünicode'
88
+ # text = bytes(str_text, 'utf-8')
89
+ num_bytes = pty.write(str_text)
90
+
91
+ line = ''
92
+ start_time = time.time()
93
+ while str_text not in line:
94
+ if time.time() - start_time > 5:
95
+ break
96
+ line += pty.read()
97
+
98
+ assert str_text in line
99
+ del pty
100
+
101
+
102
+ def test_isalive(pty_fixture):
103
+ pty = pty_fixture()
104
+ pty.write('exit\r\n')
105
+
106
+ text = 'exit'
107
+ line = ''
108
+ while text not in line:
109
+ try:
110
+ line += pty.read()
111
+ except Exception:
112
+ break
113
+
114
+ while pty.isalive():
115
+ try:
116
+ pty.read()
117
+ # continue
118
+ except Exception:
119
+ break
120
+
121
+ assert not pty.isalive()
122
+ del pty
123
+
124
+
125
+ # def test_agent_spawn_fail(pty_fixture):
126
+ # pty = pty_fixture
127
+ # try:
128
+ # pty.spawn(CMD)
129
+ # assert False
130
+ # except WinptyError:
131
+ # pass
132
+
133
+
134
+ @pytest.mark.parametrize(
135
+ 'backend_name,backend',
136
+ [("ConPTY", Backend.ConPTY), ('WinPTY', Backend.WinPTY)])
137
+ def test_pty_create_size_fail(backend_name, backend):
138
+ try:
139
+ PTY(80, -25, backend=backend)
140
+ assert False
141
+ except WinptyError:
142
+ pass
143
+
144
+
145
+ def test_agent_resize_fail(pty_fixture):
146
+ pty = pty_fixture()
147
+ try:
148
+ pty.set_size(-80, 70)
149
+ assert False
150
+ except WinptyError:
151
+ pass
152
+ finally:
153
+ del pty
154
+
155
+
156
+ def test_agent_resize(pty_fixture):
157
+ pty = pty_fixture()
158
+ pty.set_size(80, 70)
159
+ del pty
@@ -0,0 +1,260 @@
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.enums import Backend
18
+ from winpty.ptyprocess import PtyProcess, which
19
+
20
+
21
+
22
+ @pytest.fixture(scope='module', params=['WinPTY', 'ConPTY'])
23
+ def pty_fixture(request):
24
+ backend = request.param
25
+ if os.environ.get('CI_RUNNING', None) == '1':
26
+ if backend == 'ConPTY':
27
+ os.environ['CI'] = '1'
28
+ os.environ['CONPTY_CI'] = '1'
29
+ if backend == 'WinPTY':
30
+ os.environ.pop('CI', None)
31
+ os.environ.pop('CONPTY_CI', None)
32
+
33
+ backend = getattr(Backend, backend)
34
+ def _pty_factory(cmd=None, env=None):
35
+ cmd = cmd or 'cmd'
36
+ return PtyProcess.spawn(cmd, env=env, backend=backend)
37
+ _pty_factory.backend = request.param
38
+ return _pty_factory
39
+
40
+
41
+ @flaky(max_runs=40, min_passes=1)
42
+ def test_read(pty_fixture):
43
+ pty = pty_fixture()
44
+ loc = os.getcwd()
45
+ data = ''
46
+ tries = 0
47
+ while loc not in data and tries < 10:
48
+ try:
49
+ data += pty.read()
50
+ except EOFError:
51
+ pass
52
+ tries += 1
53
+ assert loc in data
54
+ pty.terminate()
55
+ time.sleep(2)
56
+
57
+
58
+ @flaky(max_runs=40, min_passes=1)
59
+ def test_write(pty_fixture):
60
+ pty = pty_fixture()
61
+
62
+ text = 'Eggs, ham and spam ünicode'
63
+ pty.write(text)
64
+
65
+ data = ''
66
+ tries = 0
67
+ while text not in data and tries < 10:
68
+ try:
69
+ data += pty.read()
70
+ except EOFError:
71
+ pass
72
+ tries += 1
73
+ assert text in data
74
+ pty.terminate()
75
+
76
+
77
+ @pytest.mark.xfail(reason="It fails sometimes due to long strings")
78
+ @flaky(max_runs=40, min_passes=1)
79
+ def test_isalive(pty_fixture):
80
+ pty = pty_fixture()
81
+
82
+ pty.write('echo \"foo\"\r\nexit\r\n')
83
+ data = ''
84
+ while True:
85
+ try:
86
+ print('Stuck')
87
+ data += pty.read()
88
+ except EOFError:
89
+ break
90
+
91
+ regex = re.compile(".*foo.*")
92
+ assert regex.findall(data)
93
+ assert not pty.isalive()
94
+ pty.terminate()
95
+
96
+
97
+ @pytest.mark.xfail(reason="It fails sometimes due to long strings")
98
+ @flaky(max_runs=40, min_passes=1)
99
+ def test_readline(pty_fixture):
100
+ env = os.environ.copy()
101
+ env['foo'] = 'bar'
102
+ pty = pty_fixture(env=env)
103
+
104
+ # Ensure that the echo print has its own CRLF
105
+ pty.write('cls\r\n')
106
+ pty.write('echo %foo%\r\n')
107
+
108
+ data = ''
109
+ tries = 0
110
+ while 'bar' not in data and tries < 10:
111
+ data = pty.readline()
112
+ tries += 1
113
+
114
+ assert 'bar' in data
115
+
116
+ pty.terminate()
117
+
118
+
119
+ def test_close(pty_fixture):
120
+ pty = pty_fixture()
121
+ pty.close()
122
+ assert not pty.isalive()
123
+
124
+
125
+ def test_flush(pty_fixture):
126
+ pty = pty_fixture()
127
+ pty.flush()
128
+ pty.terminate()
129
+
130
+
131
+ def test_intr(pty_fixture):
132
+ pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
133
+ pty.sendintr()
134
+ assert pty.wait() != 0
135
+
136
+
137
+ def test_send_control(pty_fixture):
138
+ pty = pty_fixture(cmd=[sys.executable, 'import time; time.sleep(10)'])
139
+ pty.sendcontrol('d')
140
+ assert pty.wait() != 0
141
+
142
+
143
+ @pytest.mark.skipif(which('cat') is None, reason="Requires cat on the PATH")
144
+ def test_send_eof(pty_fixture):
145
+ cat = pty_fixture('cat')
146
+ cat.sendeof()
147
+ assert cat.wait() == 0
148
+
149
+
150
+ def test_isatty(pty_fixture):
151
+ pty = pty_fixture()
152
+ assert pty.isatty()
153
+ pty.terminate()
154
+ assert not pty.isatty()
155
+
156
+
157
+ def test_wait(pty_fixture):
158
+ pty = pty_fixture(cmd=[sys.executable, '--version'])
159
+ assert pty.wait() == 0
160
+
161
+
162
+ def test_exit_status(pty_fixture):
163
+ pty = pty_fixture(cmd=[sys.executable])
164
+ pty.write('import sys;sys.exit(1)\r\n')
165
+ pty.wait()
166
+ assert pty.exitstatus == 1
167
+
168
+
169
+ @pytest.mark.timeout(30)
170
+ def test_kill_sigterm(pty_fixture):
171
+ pty = pty_fixture()
172
+ pty.write('echo \"foo\"\r\nsleep 1000\r\n')
173
+ pty.read()
174
+ pty.kill(signal.SIGTERM)
175
+
176
+ while True:
177
+ try:
178
+ pty.read()
179
+ except EOFError:
180
+ break
181
+
182
+ assert not pty.isalive()
183
+ assert pty.exitstatus == signal.SIGTERM
184
+
185
+
186
+ @pytest.mark.timeout(30)
187
+ def test_terminate(pty_fixture):
188
+ pty = pty_fixture()
189
+ pty.write('echo \"foo\"\r\nsleep 1000\r\n')
190
+ pty.read()
191
+ pty.terminate()
192
+
193
+ while True:
194
+ try:
195
+ pty.read()
196
+ except EOFError:
197
+ break
198
+
199
+ assert not pty.isalive()
200
+ assert pty.closed
201
+
202
+
203
+ @pytest.mark.timeout(30)
204
+ def test_terminate_force(pty_fixture):
205
+ pty = pty_fixture()
206
+ pty.write('echo \"foo\"\r\nsleep 1000\r\n')
207
+ pty.read()
208
+ pty.terminate(force=True)
209
+
210
+ while True:
211
+ try:
212
+ pty.read()
213
+ except EOFError:
214
+ break
215
+
216
+ assert not pty.isalive()
217
+ assert pty.closed
218
+
219
+
220
+ def test_terminate_loop(pty_fixture):
221
+ pty = pty_fixture()
222
+ loop = asyncio.SelectorEventLoop()
223
+ asyncio.set_event_loop(loop)
224
+
225
+ def reader():
226
+ try:
227
+ data = pty.read()
228
+ except EOFError:
229
+ loop.remove_reader(pty.fd)
230
+ loop.stop()
231
+
232
+ loop.add_reader(pty.fd, reader)
233
+ loop.call_soon(pty.write, 'echo \"foo\"\r\nsleep 1000\r\n')
234
+ loop.call_soon(pty.terminate, True)
235
+
236
+ try:
237
+ loop.run_forever()
238
+ finally:
239
+ loop.close()
240
+
241
+ assert not pty.isalive()
242
+ assert pty.closed
243
+
244
+
245
+ def test_getwinsize(pty_fixture):
246
+ pty = pty_fixture()
247
+ assert pty.getwinsize() == (24, 80)
248
+ pty.terminate()
249
+
250
+
251
+ def test_setwinsize(pty_fixture):
252
+ pty = pty_fixture()
253
+ pty.setwinsize(50, 110)
254
+ assert pty.getwinsize() == (50, 110)
255
+ pty.terminate()
256
+
257
+ pty = PtyProcess.spawn('cmd', dimensions=(60, 120))
258
+ assert pty.getwinsize() == (60, 120)
259
+ pty.terminate()
260
+
Binary file
Binary file
winpty/winpty.dll ADDED
Binary file
winpty/winpty.pyi ADDED
@@ -0,0 +1,52 @@
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 Backend, Encoding, MouseMode, AgentConfig
10
+
11
+
12
+ class PTY:
13
+ def __init__(self, cols: int, rows: int,
14
+ backend: Optional[int] = None,
15
+ encoding: Optional[str] = Encoding.UTF8,
16
+ mouse_mode: int = MouseMode.WINPTY_MOUSE_MODE_NONE,
17
+ timeout: int = 30000,
18
+ agent_config: int = AgentConfig.WINPTY_FLAG_COLOR_ESCAPES):
19
+ ...
20
+
21
+ def spawn(self,
22
+ appname: bytes,
23
+ cmdline: Optional[bytes] = None,
24
+ cwd: Optional[bytes] = None,
25
+ env: Optional[bytes] = None) -> bool:
26
+ ...
27
+
28
+ def set_size(self, cols: int, rows: int): ...
29
+
30
+ def read(self,
31
+ length: Optional[int] = 1000,
32
+ blocking: bool = False) -> bytes:
33
+ ...
34
+
35
+ def read_stderr(self,
36
+ length: Optional[int] = 1000,
37
+ blocking: bool = False) -> bytes:
38
+ ...
39
+
40
+ def write(self, to_write: bytes) -> int: ...
41
+
42
+ def isalive(self) -> bool: ...
43
+
44
+ def get_exitstatus(self) -> Optional[int]: ...
45
+
46
+ def iseof(self) -> bool: ...
47
+
48
+ @property
49
+ def pid(self) -> Optional[int]: ...
50
+
51
+ @property
52
+ def fd(self) -> Optional[int]: ...