Skip to content

Commit 10ddf52

Browse files
authored
Merge branch 'main' into range_iter_freelist
2 parents 0913317 + 004f9fd commit 10ddf52

File tree

14 files changed

+385
-197
lines changed

14 files changed

+385
-197
lines changed

Lib/pydoc.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class or function within a module or module in a package. If the
5353
# the current directory is changed with os.chdir(), an incorrect
5454
# path will be displayed.
5555

56+
import ast
5657
import __future__
5758
import builtins
5859
import importlib._bootstrap
@@ -384,21 +385,29 @@ def ispackage(path):
384385
return False
385386

386387
def source_synopsis(file):
387-
line = file.readline()
388-
while line[:1] == '#' or not line.strip():
389-
line = file.readline()
390-
if not line: break
391-
line = line.strip()
392-
if line[:4] == 'r"""': line = line[1:]
393-
if line[:3] == '"""':
394-
line = line[3:]
395-
if line[-1:] == '\\': line = line[:-1]
396-
while not line.strip():
397-
line = file.readline()
398-
if not line: break
399-
result = line.split('"""')[0].strip()
400-
else: result = None
401-
return result
388+
"""Return the one-line summary of a file object, if present"""
389+
390+
string = ''
391+
try:
392+
tokens = tokenize.generate_tokens(file.readline)
393+
for tok_type, tok_string, _, _, _ in tokens:
394+
if tok_type == tokenize.STRING:
395+
string += tok_string
396+
elif tok_type == tokenize.NEWLINE:
397+
with warnings.catch_warnings():
398+
# Ignore the "invalid escape sequence" warning.
399+
warnings.simplefilter("ignore", SyntaxWarning)
400+
docstring = ast.literal_eval(string)
401+
if not isinstance(docstring, str):
402+
return None
403+
return docstring.strip().split('\n')[0].strip()
404+
elif tok_type == tokenize.OP and tok_string in ('(', ')'):
405+
string += tok_string
406+
elif tok_type not in (tokenize.COMMENT, tokenize.NL, tokenize.ENCODING):
407+
return None
408+
except (tokenize.TokenError, UnicodeDecodeError, SyntaxError):
409+
return None
410+
return None
402411

403412
def synopsis(filename, cache={}):
404413
"""Get the one-line summary out of a module file."""

Lib/test/test_peepholer.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,5 +1193,56 @@ def get_insts(lno1, lno2, op1, op2):
11931193
]
11941194
self.cfg_optimization_test(insts, expected_insts, consts=list(range(5)))
11951195

1196+
def test_list_to_tuple_get_iter(self):
1197+
# for _ in (*foo, *bar) -> for _ in [*foo, *bar]
1198+
INTRINSIC_LIST_TO_TUPLE = 6
1199+
insts = [
1200+
("BUILD_LIST", 0, 1),
1201+
("LOAD_FAST", 0, 2),
1202+
("LIST_EXTEND", 1, 3),
1203+
("LOAD_FAST", 1, 4),
1204+
("LIST_EXTEND", 1, 5),
1205+
("CALL_INTRINSIC_1", INTRINSIC_LIST_TO_TUPLE, 6),
1206+
("GET_ITER", None, 7),
1207+
top := self.Label(),
1208+
("FOR_ITER", end := self.Label(), 8),
1209+
("STORE_FAST", 2, 9),
1210+
("JUMP", top, 10),
1211+
end,
1212+
("END_FOR", None, 11),
1213+
("POP_TOP", None, 12),
1214+
("LOAD_CONST", 0, 13),
1215+
("RETURN_VALUE", None, 14),
1216+
]
1217+
expected_insts = [
1218+
("BUILD_LIST", 0, 1),
1219+
("LOAD_FAST", 0, 2),
1220+
("LIST_EXTEND", 1, 3),
1221+
("LOAD_FAST", 1, 4),
1222+
("LIST_EXTEND", 1, 5),
1223+
("NOP", None, 6), # ("CALL_INTRINSIC_1", INTRINSIC_LIST_TO_TUPLE, 6),
1224+
("GET_ITER", None, 7),
1225+
top := self.Label(),
1226+
("FOR_ITER", end := self.Label(), 8),
1227+
("STORE_FAST", 2, 9),
1228+
("JUMP", top, 10),
1229+
end,
1230+
("END_FOR", None, 11),
1231+
("POP_TOP", None, 12),
1232+
("LOAD_CONST", 0, 13),
1233+
("RETURN_VALUE", None, 14),
1234+
]
1235+
self.cfg_optimization_test(insts, expected_insts, consts=[None])
1236+
1237+
def test_list_to_tuple_get_iter_is_safe(self):
1238+
a, b = [], []
1239+
for item in (*(items := [0, 1, 2, 3]),):
1240+
a.append(item)
1241+
b.append(items.pop())
1242+
self.assertEqual(a, [0, 1, 2, 3])
1243+
self.assertEqual(b, [3, 2, 1, 0])
1244+
self.assertEqual(items, [])
1245+
1246+
11961247
if __name__ == "__main__":
11971248
unittest.main()

Lib/test/test_pydoc/test_pydoc.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import contextlib
55
import importlib.util
66
import inspect
7+
import io
78
import pydoc
89
import py_compile
910
import keyword
@@ -899,6 +900,82 @@ def test_synopsis(self):
899900
synopsis = pydoc.synopsis(TESTFN, {})
900901
self.assertEqual(synopsis, 'line 1: h\xe9')
901902

903+
def test_source_synopsis(self):
904+
def check(source, expected, encoding=None):
905+
if isinstance(source, str):
906+
source_file = StringIO(source)
907+
else:
908+
source_file = io.TextIOWrapper(io.BytesIO(source), encoding=encoding)
909+
with source_file:
910+
result = pydoc.source_synopsis(source_file)
911+
self.assertEqual(result, expected)
912+
913+
check('"""Single line docstring."""',
914+
'Single line docstring.')
915+
check('"""First line of docstring.\nSecond line.\nThird line."""',
916+
'First line of docstring.')
917+
check('"""First line of docstring.\\nSecond line.\\nThird line."""',
918+
'First line of docstring.')
919+
check('""" Whitespace around docstring. """',
920+
'Whitespace around docstring.')
921+
check('import sys\n"""No docstring"""',
922+
None)
923+
check(' \n"""Docstring after empty line."""',
924+
'Docstring after empty line.')
925+
check('# Comment\n"""Docstring after comment."""',
926+
'Docstring after comment.')
927+
check(' # Indented comment\n"""Docstring after comment."""',
928+
'Docstring after comment.')
929+
check('""""""', # Empty docstring
930+
'')
931+
check('', # Empty file
932+
None)
933+
check('"""Embedded\0null byte"""',
934+
None)
935+
check('"""Embedded null byte"""\0',
936+
None)
937+
check('"""Café and résumé."""',
938+
'Café and résumé.')
939+
check("'''Triple single quotes'''",
940+
'Triple single quotes')
941+
check('"Single double quotes"',
942+
'Single double quotes')
943+
check("'Single single quotes'",
944+
'Single single quotes')
945+
check('"""split\\\nline"""',
946+
'splitline')
947+
check('"""Unrecognized escape \\sequence"""',
948+
'Unrecognized escape \\sequence')
949+
check('"""Invalid escape seq\\uence"""',
950+
None)
951+
check('r"""Raw \\stri\\ng"""',
952+
'Raw \\stri\\ng')
953+
check('b"""Bytes literal"""',
954+
None)
955+
check('f"""f-string"""',
956+
None)
957+
check('"""Concatenated""" \\\n"string" \'literals\'',
958+
'Concatenatedstringliterals')
959+
check('"""String""" + """expression"""',
960+
None)
961+
check('("""In parentheses""")',
962+
'In parentheses')
963+
check('("""Multiple lines """\n"""in parentheses""")',
964+
'Multiple lines in parentheses')
965+
check('()', # tuple
966+
None)
967+
check(b'# coding: iso-8859-15\n"""\xa4uro sign"""',
968+
'€uro sign', encoding='iso-8859-15')
969+
check(b'"""\xa4"""', # Decoding error
970+
None, encoding='utf-8')
971+
972+
with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8') as temp_file:
973+
temp_file.write('"""Real file test."""\n')
974+
temp_file.flush()
975+
temp_file.seek(0)
976+
result = pydoc.source_synopsis(temp_file)
977+
self.assertEqual(result, "Real file test.")
978+
902979
@requires_docstrings
903980
def test_synopsis_sourceless(self):
904981
os = import_helper.import_fresh_module('os')

Lib/test/test_typing.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5182,6 +5182,18 @@ class C(B[int]):
51825182
x = pickle.loads(z)
51835183
self.assertEqual(s, x)
51845184

5185+
# Test ParamSpec args and kwargs
5186+
global PP
5187+
PP = ParamSpec('PP')
5188+
for thing in [PP.args, PP.kwargs]:
5189+
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
5190+
with self.subTest(thing=thing, proto=proto):
5191+
self.assertEqual(
5192+
pickle.loads(pickle.dumps(thing, proto)),
5193+
thing,
5194+
)
5195+
del PP
5196+
51855197
def test_copy_and_deepcopy(self):
51865198
T = TypeVar('T')
51875199
class Node(Generic[T]): ...
@@ -8912,13 +8924,13 @@ class Child1(Base1):
89128924
self.assertEqual(Child1.__mutable_keys__, frozenset({'b'}))
89138925

89148926
class Base2(TypedDict):
8915-
a: ReadOnly[int]
8927+
a: int
89168928

89178929
class Child2(Base2):
8918-
b: str
8930+
b: ReadOnly[str]
89198931

8920-
self.assertEqual(Child1.__readonly_keys__, frozenset({'a'}))
8921-
self.assertEqual(Child1.__mutable_keys__, frozenset({'b'}))
8932+
self.assertEqual(Child2.__readonly_keys__, frozenset({'b'}))
8933+
self.assertEqual(Child2.__mutable_keys__, frozenset({'a'}))
89228934

89238935
def test_cannot_make_mutable_key_readonly(self):
89248936
class Base(TypedDict):
@@ -10129,6 +10141,18 @@ def test_valid_uses(self):
1012910141
self.assertEqual(C4.__args__, (Concatenate[int, T, P], T))
1013010142
self.assertEqual(C4.__parameters__, (T, P))
1013110143

10144+
def test_invalid_uses(self):
10145+
with self.assertRaisesRegex(TypeError, 'Concatenate of no types'):
10146+
Concatenate[()]
10147+
with self.assertRaisesRegex(
10148+
TypeError,
10149+
(
10150+
'The last parameter to Concatenate should be a '
10151+
'ParamSpec variable or ellipsis'
10152+
),
10153+
):
10154+
Concatenate[int]
10155+
1013210156
def test_var_substitution(self):
1013310157
T = TypeVar('T')
1013410158
P = ParamSpec('P')
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
We now use the location of the ``libpython`` runtime library used in the current
2+
proccess to determine :data:`sys.base_prefix` on all platforms implementing the
3+
`dladdr <https://pubs.opengroup.org/onlinepubs/9799919799/functions/dladdr.html>`_
4+
function defined by the UNIX standard — this includes Linux, Android, macOS,
5+
iOS, FreeBSD, etc. This was already the case on Windows and macOS Framework
6+
builds.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix quick extraction of module docstrings from a file in :mod:`pydoc`.
2+
It now supports docstrings with single quotes, escape sequences,
3+
raw string literals, and other Python syntax.

Modules/getpath.c

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717
#endif
1818

1919
#ifdef __APPLE__
20-
# include <dlfcn.h>
2120
# include <mach-o/dyld.h>
2221
#endif
2322

23+
#ifdef HAVE_DLFCN_H
24+
# include <dlfcn.h>
25+
#endif
26+
2427
/* Reference the precompiled getpath.py */
2528
#include "Python/frozen_modules/getpath.h"
2629

@@ -803,36 +806,25 @@ progname_to_dict(PyObject *dict, const char *key)
803806
static int
804807
library_to_dict(PyObject *dict, const char *key)
805808
{
809+
/* macOS framework builds do not link against a libpython dynamic library, but
810+
instead link against a macOS Framework. */
811+
#if defined(Py_ENABLE_SHARED) || defined(WITH_NEXT_FRAMEWORK)
812+
806813
#ifdef MS_WINDOWS
807-
#ifdef Py_ENABLE_SHARED
808814
extern HMODULE PyWin_DLLhModule;
809815
if (PyWin_DLLhModule) {
810816
return winmodule_to_dict(dict, key, PyWin_DLLhModule);
811817
}
812818
#endif
813-
#elif defined(WITH_NEXT_FRAMEWORK)
814-
static char modPath[MAXPATHLEN + 1];
815-
static int modPathInitialized = -1;
816-
if (modPathInitialized < 0) {
817-
modPathInitialized = 0;
818-
819-
/* On Mac OS X we have a special case if we're running from a framework.
820-
This is because the python home should be set relative to the library,
821-
which is in the framework, not relative to the executable, which may
822-
be outside of the framework. Except when we're in the build
823-
directory... */
824-
Dl_info pythonInfo;
825-
if (dladdr(&Py_Initialize, &pythonInfo)) {
826-
if (pythonInfo.dli_fname) {
827-
strncpy(modPath, pythonInfo.dli_fname, MAXPATHLEN);
828-
modPathInitialized = 1;
829-
}
830-
}
831-
}
832-
if (modPathInitialized > 0) {
833-
return decode_to_dict(dict, key, modPath);
819+
820+
#if HAVE_DLADDR
821+
Dl_info libpython_info;
822+
if (dladdr(&Py_Initialize, &libpython_info) && libpython_info.dli_fname) {
823+
return decode_to_dict(dict, key, libpython_info.dli_fname);
834824
}
835825
#endif
826+
#endif
827+
836828
return PyDict_SetItemString(dict, key, Py_None) == 0;
837829
}
838830

0 commit comments

Comments
 (0)