Skip to content

Commit 312c790

Browse files
authored
Merge branch 'main' into small_int_immortal
2 parents ea17aee + 7725c03 commit 312c790

File tree

6 files changed

+492
-249
lines changed

6 files changed

+492
-249
lines changed

Doc/library/shutil.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -491,12 +491,6 @@ Directory and files operations
491491
or ends with an extension that is in ``PATHEXT``; and filenames that
492492
have no extension can now be found.
493493

494-
.. versionchanged:: 3.12.1
495-
On Windows, if *mode* includes ``os.X_OK``, executables with an
496-
extension in ``PATHEXT`` will be preferred over executables without a
497-
matching extension.
498-
This brings behavior closer to that of Python 3.11.
499-
500494
.. exception:: Error
501495

502496
This exception collects exceptions that are raised during a multi-file

Lib/shutil.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,21 +1550,21 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
15501550
if sys.platform == "win32":
15511551
# PATHEXT is necessary to check on Windows.
15521552
pathext_source = os.getenv("PATHEXT") or _WIN_DEFAULT_PATHEXT
1553-
pathext = [ext for ext in pathext_source.split(os.pathsep) if ext]
1553+
pathext = pathext_source.split(os.pathsep)
1554+
pathext = [ext.rstrip('.') for ext in pathext if ext]
15541555

15551556
if use_bytes:
15561557
pathext = [os.fsencode(ext) for ext in pathext]
15571558

1558-
files = ([cmd] + [cmd + ext for ext in pathext])
1559+
files = [cmd + ext for ext in pathext]
15591560

1560-
# gh-109590. If we are looking for an executable, we need to look
1561-
# for a PATHEXT match. The first cmd is the direct match
1562-
# (e.g. python.exe instead of python)
1563-
# Check that direct match first if and only if the extension is in PATHEXT
1564-
# Otherwise check it last
1565-
suffix = os.path.splitext(files[0])[1].upper()
1566-
if mode & os.X_OK and not any(suffix == ext.upper() for ext in pathext):
1567-
files.append(files.pop(0))
1561+
# If X_OK in mode, simulate the cmd.exe behavior: look at direct
1562+
# match if and only if the extension is in PATHEXT.
1563+
# If X_OK not in mode, simulate the first result of where.exe:
1564+
# always look at direct match before a PATHEXT match.
1565+
normcmd = cmd.upper()
1566+
if not (mode & os.X_OK) or any(normcmd.endswith(ext.upper()) for ext in pathext):
1567+
files.insert(0, cmd)
15681568
else:
15691569
# On other platforms you don't have things like PATHEXT to tell you
15701570
# what file suffixes are executable, so just pass on cmd as-is.
@@ -1573,7 +1573,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
15731573
seen = set()
15741574
for dir in path:
15751575
normdir = os.path.normcase(dir)
1576-
if not normdir in seen:
1576+
if normdir not in seen:
15771577
seen.add(normdir)
15781578
for thefile in files:
15791579
name = os.path.join(dir, thefile)

Lib/test/libregrtest/run_workers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def get_running(workers: list[WorkerThread]) -> str | None:
457457
running: list[str] = []
458458
for worker in workers:
459459
test_name = worker.test_name
460-
if not test_name:
460+
if test_name == _NOT_RUNNING:
461461
continue
462462
dt = time.monotonic() - worker.start_time
463463
if dt >= PROGRESS_MIN_TIME:
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import ctypes
2+
import gc
3+
import sys
4+
import unittest
5+
from ctypes import POINTER, byref, c_void_p
6+
from ctypes.wintypes import BYTE, DWORD, WORD
7+
8+
if sys.platform != "win32":
9+
raise unittest.SkipTest("Windows-specific test")
10+
11+
12+
from _ctypes import COMError
13+
from ctypes import HRESULT
14+
15+
16+
COINIT_APARTMENTTHREADED = 0x2
17+
CLSCTX_SERVER = 5
18+
S_OK = 0
19+
OUT = 2
20+
TRUE = 1
21+
E_NOINTERFACE = -2147467262
22+
23+
24+
class GUID(ctypes.Structure):
25+
# https://learn.microsoft.com/en-us/windows/win32/api/guiddef/ns-guiddef-guid
26+
_fields_ = [
27+
("Data1", DWORD),
28+
("Data2", WORD),
29+
("Data3", WORD),
30+
("Data4", BYTE * 8),
31+
]
32+
33+
34+
def create_proto_com_method(name, index, restype, *argtypes):
35+
proto = ctypes.WINFUNCTYPE(restype, *argtypes)
36+
37+
def make_method(*args):
38+
foreign_func = proto(index, name, *args)
39+
40+
def call(self, *args, **kwargs):
41+
return foreign_func(self, *args, **kwargs)
42+
43+
return call
44+
45+
return make_method
46+
47+
48+
def create_guid(name):
49+
guid = GUID()
50+
# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-clsidfromstring
51+
ole32.CLSIDFromString(name, byref(guid))
52+
return guid
53+
54+
55+
def is_equal_guid(guid1, guid2):
56+
# https://learn.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-isequalguid
57+
return ole32.IsEqualGUID(byref(guid1), byref(guid2))
58+
59+
60+
ole32 = ctypes.oledll.ole32
61+
62+
IID_IUnknown = create_guid("{00000000-0000-0000-C000-000000000046}")
63+
IID_IStream = create_guid("{0000000C-0000-0000-C000-000000000046}")
64+
IID_IPersist = create_guid("{0000010C-0000-0000-C000-000000000046}")
65+
CLSID_ShellLink = create_guid("{00021401-0000-0000-C000-000000000046}")
66+
67+
# https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void)
68+
proto_query_interface = create_proto_com_method(
69+
"QueryInterface", 0, HRESULT, POINTER(GUID), POINTER(c_void_p)
70+
)
71+
# https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-addref
72+
proto_add_ref = create_proto_com_method("AddRef", 1, ctypes.c_long)
73+
# https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-release
74+
proto_release = create_proto_com_method("Release", 2, ctypes.c_long)
75+
# https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-ipersist-getclassid
76+
proto_get_class_id = create_proto_com_method(
77+
"GetClassID", 3, HRESULT, POINTER(GUID)
78+
)
79+
80+
81+
class ForeignFunctionsThatWillCallComMethodsTests(unittest.TestCase):
82+
def setUp(self):
83+
# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coinitializeex
84+
ole32.CoInitializeEx(None, COINIT_APARTMENTTHREADED)
85+
86+
def tearDown(self):
87+
# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-couninitialize
88+
ole32.CoUninitialize()
89+
gc.collect()
90+
91+
@staticmethod
92+
def create_shelllink_persist(typ):
93+
ppst = typ()
94+
# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance
95+
ole32.CoCreateInstance(
96+
byref(CLSID_ShellLink),
97+
None,
98+
CLSCTX_SERVER,
99+
byref(IID_IPersist),
100+
byref(ppst),
101+
)
102+
return ppst
103+
104+
def test_without_paramflags_and_iid(self):
105+
class IUnknown(c_void_p):
106+
QueryInterface = proto_query_interface()
107+
AddRef = proto_add_ref()
108+
Release = proto_release()
109+
110+
class IPersist(IUnknown):
111+
GetClassID = proto_get_class_id()
112+
113+
ppst = self.create_shelllink_persist(IPersist)
114+
115+
clsid = GUID()
116+
hr_getclsid = ppst.GetClassID(byref(clsid))
117+
self.assertEqual(S_OK, hr_getclsid)
118+
self.assertEqual(TRUE, is_equal_guid(CLSID_ShellLink, clsid))
119+
120+
self.assertEqual(2, ppst.AddRef())
121+
self.assertEqual(3, ppst.AddRef())
122+
123+
punk = IUnknown()
124+
hr_qi = ppst.QueryInterface(IID_IUnknown, punk)
125+
self.assertEqual(S_OK, hr_qi)
126+
self.assertEqual(3, punk.Release())
127+
128+
with self.assertRaises(OSError) as e:
129+
punk.QueryInterface(IID_IStream, IUnknown())
130+
self.assertEqual(E_NOINTERFACE, e.exception.winerror)
131+
132+
self.assertEqual(2, ppst.Release())
133+
self.assertEqual(1, ppst.Release())
134+
self.assertEqual(0, ppst.Release())
135+
136+
def test_with_paramflags_and_without_iid(self):
137+
class IUnknown(c_void_p):
138+
QueryInterface = proto_query_interface(None)
139+
AddRef = proto_add_ref()
140+
Release = proto_release()
141+
142+
class IPersist(IUnknown):
143+
GetClassID = proto_get_class_id(((OUT, "pClassID"),))
144+
145+
ppst = self.create_shelllink_persist(IPersist)
146+
147+
clsid = ppst.GetClassID()
148+
self.assertEqual(TRUE, is_equal_guid(CLSID_ShellLink, clsid))
149+
150+
punk = IUnknown()
151+
hr_qi = ppst.QueryInterface(IID_IUnknown, punk)
152+
self.assertEqual(S_OK, hr_qi)
153+
self.assertEqual(1, punk.Release())
154+
155+
with self.assertRaises(OSError) as e:
156+
ppst.QueryInterface(IID_IStream, IUnknown())
157+
self.assertEqual(E_NOINTERFACE, e.exception.winerror)
158+
159+
self.assertEqual(0, ppst.Release())
160+
161+
def test_with_paramflags_and_iid(self):
162+
class IUnknown(c_void_p):
163+
QueryInterface = proto_query_interface(None, IID_IUnknown)
164+
AddRef = proto_add_ref()
165+
Release = proto_release()
166+
167+
class IPersist(IUnknown):
168+
GetClassID = proto_get_class_id(((OUT, "pClassID"),), IID_IPersist)
169+
170+
ppst = self.create_shelllink_persist(IPersist)
171+
172+
clsid = ppst.GetClassID()
173+
self.assertEqual(TRUE, is_equal_guid(CLSID_ShellLink, clsid))
174+
175+
punk = IUnknown()
176+
hr_qi = ppst.QueryInterface(IID_IUnknown, punk)
177+
self.assertEqual(S_OK, hr_qi)
178+
self.assertEqual(1, punk.Release())
179+
180+
with self.assertRaises(COMError) as e:
181+
ppst.QueryInterface(IID_IStream, IUnknown())
182+
self.assertEqual(E_NOINTERFACE, e.exception.hresult)
183+
184+
self.assertEqual(0, ppst.Release())
185+
186+
187+
if __name__ == '__main__':
188+
unittest.main()

0 commit comments

Comments
 (0)