Skip to content

Commit f5f0aeb

Browse files
authored
Switch from pywin32 to ctype (#5)
pywin32 is not required to access Windows API
1 parent f002518 commit f5f0aeb

File tree

3 files changed

+67
-55
lines changed

3 files changed

+67
-55
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,4 @@ cython_debug/
171171

172172
# Built Visual Studio Code Extensions
173173
*.vsix
174+
/.idea

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ classifiers = [
2222
]
2323
dynamic = ["version"]
2424
requires-python = ">=3.9"
25-
dependencies = ["pywin32;platform_system=='Windows'"]
25+
dependencies = []
2626

2727
[project.urls]
2828
Repository = "https://github.com/python-ffmpegio/python-namedpipe"

src/namedpipe/_win32.py

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1-
import win32pipe, win32file, win32api, winerror, win32con, pywintypes
1+
import ctypes
2+
from ctypes import wintypes
23
from os import path
34
import io
45
from typing import TypeVar, NewType, Literal, IO, Optional, Union
56

67
WritableBuffer = TypeVar("WritableBuffer")
78
PyHANDLE = NewType("PyHANDLE", int)
89

10+
# Define global constants
11+
PIPE_ACCESS_DUPLEX = 0x00000003
12+
PIPE_ACCESS_INBOUND = 0x00000001
13+
PIPE_ACCESS_OUTBOUND = 0x00000002
14+
PIPE_TYPE_BYTE = 0x00000000
15+
PIPE_READMODE_BYTE = 0x00000000
16+
PIPE_WAIT = 0x00000000
17+
PIPE_UNLIMITED_INSTANCES = 0xFFFFFFFF
18+
ERROR_PIPE_CONNECTED = 535
19+
ERROR_BROKEN_PIPE = 109
20+
ERROR_MORE_DATA = 234
21+
ERROR_IO_PENDING = 997
22+
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
23+
INVALID_HANDLE_VALUE = -1
24+
925
id = 0
1026

27+
def _wt(value: int) -> wintypes.DWORD:
28+
return wintypes.DWORD(value)
1129

1230
def _name_pipe():
1331
global id
@@ -23,11 +41,8 @@ def _name_pipe():
2341

2442
def _win_error(code=None):
2543
if not code:
26-
code = win32api.GetLastError()
27-
return OSError(
28-
f"[OS Error {code}] {win32api.FormatMessage(win32con.FORMAT_MESSAGE_FROM_SYSTEM,0,code,0,None)}"
29-
)
30-
44+
code = ctypes.get_last_error()
45+
return ctypes.WinError(code)
3146

3247
class NPopen:
3348
def __init__(
@@ -44,6 +59,7 @@ def __init__(
4459
if not isinstance(bufsize, int):
4560
raise TypeError("bufsize must be an integer")
4661

62+
self.kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
4763
self.stream: Union[IO, None] = None # I/O stream of the pipe
4864
self._path = _name_pipe() if name is None else rf"\\.\pipe\{name}"
4965
self._rd = any(c in mode for c in "r+")
@@ -72,30 +88,25 @@ def __init__(
7288
)
7389

7490
self._bufsize = -1 if txt_mode and bufsize == 1 else bufsize
75-
7691
if self._rd and self._wr:
77-
access = win32pipe.PIPE_ACCESS_DUPLEX
92+
access = _wt(PIPE_ACCESS_DUPLEX)
7893
elif self._rd:
79-
access = win32pipe.PIPE_ACCESS_INBOUND
94+
access = _wt(PIPE_ACCESS_INBOUND)
8095
elif self._wr:
81-
access = win32pipe.PIPE_ACCESS_OUTBOUND
96+
access = _wt(PIPE_ACCESS_OUTBOUND)
8297
else:
8398
raise ValueError("Invalid mode")
8499
# TODO: assess options: FILE_FLAG_WRITE_THROUGH, FILE_FLAG_OVERLAPPED, FILE_FLAG_FIRST_PIPE_INSTANCE
100+
pipe_mode = _wt(PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT)
85101

86-
pipe_mode = (
87-
win32pipe.PIPE_TYPE_BYTE
88-
| win32pipe.PIPE_READMODE_BYTE
89-
| win32pipe.PIPE_WAIT
90-
)
91102
# TODO: assess options: PIPE_WAIT, PIPE_NOWAIT, PIPE_ACCEPT_REMOTE_CLIENTS, PIPE_REJECT_REMOTE_CLIENTS
92103

93-
max_instances = win32pipe.PIPE_UNLIMITED_INSTANCES # 1
94-
buffer_size = 0
95-
timeout = 0
104+
max_instances = _wt(1) # PIPE_UNLIMITED_INSTANCES returns 'invalid params'. Pipes are point-to-point anyway
105+
buffer_size = _wt(0)
106+
timeout = _wt(0)
96107

97108
# "open" named pipe
98-
self._pipe = win32pipe.CreateNamedPipe(
109+
h = self.kernel32.CreateNamedPipeW(
99110
self._path,
100111
access,
101112
pipe_mode,
@@ -105,37 +116,37 @@ def __init__(
105116
timeout,
106117
None,
107118
)
108-
if self._pipe == win32file.INVALID_HANDLE_VALUE:
119+
if h == INVALID_HANDLE_VALUE:
109120
raise _win_error()
121+
self._pipe = h
110122

111123
@property
112124
def path(self):
113125
"""str: path of the pipe in the file system"""
114126
return self._path
115127

116128
def __str__(self):
117-
# return the path
118129
return self._path
119130

120131
def close(self):
121-
# close named pipe
132+
"""Close the named pipe.
133+
A closed pipe cannot be used for further I/O operations. `close()` may
134+
be called more than once without error.
135+
"""
122136
if self.stream is not None:
123137
self.stream.close()
124138
self.stream = None
125139
if self._pipe is not None:
126-
if win32file.CloseHandle(self._pipe):
127-
raise _win_error()
140+
self.kernel32.CloseHandle(self._pipe)
128141
self._pipe = None
129142

130143
def wait(self):
131-
144+
"""Wait for the pipe to open (the other end to be opened) and return file object to read/write."""
132145
if not self._pipe:
133146
raise RuntimeError("pipe has already been closed.")
134-
135-
# wait for the pipe to open (the other end to be opened) and return fileobj to read/write
136-
if win32pipe.ConnectNamedPipe(self._pipe, None):
137-
code = win32api.GetLastError()
138-
if code != 535: # ERROR_PIPE_CONNECTED (ok, just indicating that the client has already connected)(Issue#3)
147+
if not self.kernel32.ConnectNamedPipe(self._pipe, None):
148+
code = ctypes.get_last_error()
149+
if code != ERROR_PIPE_CONNECTED: # (ok, just indicating that the client has already connected)(Issue#3)
139150
raise _win_error(code)
140151

141152
# create new io stream object
@@ -179,15 +190,16 @@ def writable(self) -> bool:
179190
class Win32RawIO(io.RawIOBase):
180191
"""Raw I/O stream layer over open Windows pipe handle.
181192
182-
`handle` is an open ``pywintypes.PyHANDLE`` object (from ``pywin32`` package) to
193+
`handle` is an open Windows ``HANDLE`` object (from ``ctype`` package) to
183194
be wrapped by this class.
184195
185196
Specify the read and write modes by boolean flags: ``rd`` and ``wr``.
186197
"""
187198

188199
def __init__(self, handle: PyHANDLE, rd: bool, wr: bool) -> None:
189200
super().__init__()
190-
self.handle = handle # PyHANDLE: Underlying Windows handle.
201+
self.kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
202+
self.handle = handle # Underlying Windows handle.
191203
self._readable: bool = rd
192204
self._writable: bool = wr
193205

@@ -211,7 +223,7 @@ def close(self) -> None:
211223
if self.closed:
212224
return
213225
if self.handle is not None:
214-
win32file.CloseHandle(self.handle)
226+
self.kernel32.CloseHandle(self.handle)
215227
self.handle = None
216228

217229
super().close()
@@ -224,23 +236,19 @@ def readinto(self, b: WritableBuffer) -> Union[int, None]:
224236
assert self._readable
225237

226238
size = len(b)
227-
nread = 0
228-
while nread < size:
229-
try:
230-
hr, res = win32file.ReadFile(self.handle, size - nread)
231-
if hr in (winerror.ERROR_MORE_DATA, winerror.ERROR_IO_PENDING):
232-
raise _win_error(hr)
233-
except pywintypes.error as e:
234-
if e.args[0] == 109: # broken pipe error
235-
break
236-
else:
237-
raise e
238-
if not len(res):
239-
break
240-
nnext = nread + len(res)
241-
b[nread:nnext] = res
242-
nread = nnext
243-
return nread
239+
nread = _wt(0)
240+
buf = (ctypes.c_char * size).from_buffer(b)
241+
242+
success = self.kernel32.ReadFile(self.handle, buf, size, ctypes.byref(nread), None)
243+
if not success:
244+
code = ctypes.get_last_error()
245+
# ERROR_MORE_DATA - not big deal, will read next time
246+
# ERROR_IO_PENDING - should not happen, unless use OVERLAPPING, which we don't so far
247+
# ERROR_BROKEN_PIPE - pipe was closed from other end. While it is an error, test seemingly expects to receive 0 instead of exception
248+
if code not in (ERROR_MORE_DATA, ERROR_IO_PENDING, ERROR_BROKEN_PIPE):
249+
raise _win_error(code)
250+
251+
return nread.value
244252

245253
def write(self, b):
246254
"""Write buffer ``b`` to file, return number of bytes written.
@@ -250,7 +258,10 @@ def write(self, b):
250258

251259
assert self.handle is not None # show type checkers we already checked
252260
assert self._writable
253-
hr, n_written = win32file.WriteFile(self.handle, b)
254-
if hr:
255-
raise _win_error(hr)
256-
return n_written
261+
size = len(b)
262+
nwritten = _wt(0)
263+
buf = (ctypes.c_char * size).from_buffer_copy(b)
264+
if not self.kernel32.WriteFile(self.handle, buf, _wt(size), ctypes.byref(nwritten), None):
265+
raise _win_error()
266+
267+
return nwritten.value

0 commit comments

Comments
 (0)