Skip to content

Commit d863a4b

Browse files
authored
Merge branch 'main' into asyncio-communicate-close
2 parents 4bf5647 + 9169a56 commit d863a4b

File tree

17 files changed

+210
-88
lines changed

17 files changed

+210
-88
lines changed

Doc/library/unittest.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2281,7 +2281,8 @@ Loading and running tests
22812281

22822282
The *testRunner* argument can either be a test runner class or an already
22832283
created instance of it. By default ``main`` calls :func:`sys.exit` with
2284-
an exit code indicating success or failure of the tests run.
2284+
an exit code indicating success (0) or failure (1) of the tests run.
2285+
An exit code of 5 indicates that no tests were run.
22852286

22862287
The *testLoader* argument has to be a :class:`TestLoader` instance,
22872288
and defaults to :data:`defaultTestLoader`.

Grammar/python.gram

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -881,14 +881,13 @@ fstring_middle[expr_ty]:
881881
| fstring_replacement_field
882882
| t=FSTRING_MIDDLE { _PyPegen_constant_from_token(p, t) }
883883
fstring_replacement_field[expr_ty]:
884-
| '{' a=(yield_expr | star_expressions) debug_expr="="? conversion=[fstring_conversion] format=[fstring_full_format_spec] '}' {
885-
_PyPegen_formatted_value(p, a, debug_expr, conversion, format, EXTRA)
886-
}
884+
| '{' a=(yield_expr | star_expressions) debug_expr="="? conversion=[fstring_conversion] format=[fstring_full_format_spec] rbrace='}' {
885+
_PyPegen_formatted_value(p, a, debug_expr, conversion, format, rbrace, EXTRA) }
887886
| invalid_replacement_field
888-
fstring_conversion[expr_ty]:
887+
fstring_conversion[ResultTokenWithMetadata*]:
889888
| conv_token="!" conv=NAME { _PyPegen_check_fstring_conversion(p, conv_token, conv) }
890-
fstring_full_format_spec[expr_ty]:
891-
| ':' spec=fstring_format_spec* { spec ? _PyAST_JoinedStr((asdl_expr_seq*)spec, EXTRA) : NULL }
889+
fstring_full_format_spec[ResultTokenWithMetadata*]:
890+
| colon=':' spec=fstring_format_spec* { _PyPegen_setup_full_format_spec(p, colon, (asdl_expr_seq *) spec, EXTRA) }
892891
fstring_format_spec[expr_ty]:
893892
| t=FSTRING_MIDDLE { _PyPegen_constant_from_token(p, t) }
894893
| fstring_replacement_field

Lib/test/test_fstring.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1535,5 +1535,19 @@ def test_not_closing_quotes(self):
15351535
self.assertAllRaise(SyntaxError, "unterminated triple-quoted f-string literal",
15361536
['f"""', "f'''"])
15371537

1538+
def test_syntax_error_after_debug(self):
1539+
self.assertAllRaise(SyntaxError, "f-string: expecting a valid expression after '{'",
1540+
[
1541+
"f'{1=}{;'",
1542+
"f'{1=}{+;'",
1543+
"f'{1=}{2}{;'",
1544+
"f'{1=}{3}{;'",
1545+
])
1546+
self.assertAllRaise(SyntaxError, "f-string: expecting '=', or '!', or ':', or '}'",
1547+
[
1548+
"f'{1=}{1;'",
1549+
"f'{1=}{1;}'",
1550+
])
1551+
15381552
if __name__ == '__main__':
15391553
unittest.main()

Lib/test/test_unittest/test_program.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,22 @@ def testExpectedFailure(self):
7171
def testUnexpectedSuccess(self):
7272
pass
7373

74-
class FooBarLoader(unittest.TestLoader):
75-
"""Test loader that returns a suite containing FooBar."""
74+
class Empty(unittest.TestCase):
75+
pass
76+
77+
class TestLoader(unittest.TestLoader):
78+
"""Test loader that returns a suite containing the supplied testcase."""
79+
80+
def __init__(self, testcase):
81+
self.testcase = testcase
82+
7683
def loadTestsFromModule(self, module):
7784
return self.suiteClass(
78-
[self.loadTestsFromTestCase(Test_TestProgram.FooBar)])
85+
[self.loadTestsFromTestCase(self.testcase)])
7986

8087
def loadTestsFromNames(self, names, module):
8188
return self.suiteClass(
82-
[self.loadTestsFromTestCase(Test_TestProgram.FooBar)])
89+
[self.loadTestsFromTestCase(self.testcase)])
8390

8491
def test_defaultTest_with_string(self):
8592
class FakeRunner(object):
@@ -92,7 +99,7 @@ def run(self, test):
9299
runner = FakeRunner()
93100
program = unittest.TestProgram(testRunner=runner, exit=False,
94101
defaultTest='test.test_unittest',
95-
testLoader=self.FooBarLoader())
102+
testLoader=self.TestLoader(self.FooBar))
96103
sys.argv = old_argv
97104
self.assertEqual(('test.test_unittest',), program.testNames)
98105

@@ -108,7 +115,7 @@ def run(self, test):
108115
program = unittest.TestProgram(
109116
testRunner=runner, exit=False,
110117
defaultTest=['test.test_unittest', 'test.test_unittest2'],
111-
testLoader=self.FooBarLoader())
118+
testLoader=self.TestLoader(self.FooBar))
112119
sys.argv = old_argv
113120
self.assertEqual(['test.test_unittest', 'test.test_unittest2'],
114121
program.testNames)
@@ -118,7 +125,7 @@ def test_NonExit(self):
118125
program = unittest.main(exit=False,
119126
argv=["foobar"],
120127
testRunner=unittest.TextTestRunner(stream=stream),
121-
testLoader=self.FooBarLoader())
128+
testLoader=self.TestLoader(self.FooBar))
122129
self.assertTrue(hasattr(program, 'result'))
123130
out = stream.getvalue()
124131
self.assertIn('\nFAIL: testFail ', out)
@@ -130,13 +137,13 @@ def test_NonExit(self):
130137

131138
def test_Exit(self):
132139
stream = BufferedWriter()
133-
self.assertRaises(
134-
SystemExit,
135-
unittest.main,
136-
argv=["foobar"],
137-
testRunner=unittest.TextTestRunner(stream=stream),
138-
exit=True,
139-
testLoader=self.FooBarLoader())
140+
with self.assertRaises(SystemExit) as cm:
141+
unittest.main(
142+
argv=["foobar"],
143+
testRunner=unittest.TextTestRunner(stream=stream),
144+
exit=True,
145+
testLoader=self.TestLoader(self.FooBar))
146+
self.assertEqual(cm.exception.code, 1)
140147
out = stream.getvalue()
141148
self.assertIn('\nFAIL: testFail ', out)
142149
self.assertIn('\nERROR: testError ', out)
@@ -147,12 +154,11 @@ def test_Exit(self):
147154

148155
def test_ExitAsDefault(self):
149156
stream = BufferedWriter()
150-
self.assertRaises(
151-
SystemExit,
152-
unittest.main,
153-
argv=["foobar"],
154-
testRunner=unittest.TextTestRunner(stream=stream),
155-
testLoader=self.FooBarLoader())
157+
with self.assertRaises(SystemExit):
158+
unittest.main(
159+
argv=["foobar"],
160+
testRunner=unittest.TextTestRunner(stream=stream),
161+
testLoader=self.TestLoader(self.FooBar))
156162
out = stream.getvalue()
157163
self.assertIn('\nFAIL: testFail ', out)
158164
self.assertIn('\nERROR: testError ', out)
@@ -161,6 +167,17 @@ def test_ExitAsDefault(self):
161167
'expected failures=1, unexpected successes=1)\n')
162168
self.assertTrue(out.endswith(expected))
163169

170+
def test_ExitEmptySuite(self):
171+
stream = BufferedWriter()
172+
with self.assertRaises(SystemExit) as cm:
173+
unittest.main(
174+
argv=["empty"],
175+
testRunner=unittest.TextTestRunner(stream=stream),
176+
testLoader=self.TestLoader(self.Empty))
177+
self.assertEqual(cm.exception.code, 5)
178+
out = stream.getvalue()
179+
self.assertIn('\nNO TESTS RAN\n', out)
180+
164181

165182
class InitialisableProgram(unittest.TestProgram):
166183
exit = False

Lib/test/test_unittest/test_result.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ def testFailFastSetByRunner(self):
451451
stream = BufferedWriter()
452452
runner = unittest.TextTestRunner(stream=stream, failfast=True)
453453
def test(result):
454+
result.testsRun += 1
454455
self.assertTrue(result.failfast)
455456
result = runner.run(test)
456457
stream.flush()

Lib/test/test_unittest/test_runner.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,16 @@ def test(self):
577577
'inner setup', 'inner test', 'inner cleanup',
578578
'end outer test', 'outer cleanup'])
579579

580+
def test_run_empty_suite_error_message(self):
581+
class EmptyTest(unittest.TestCase):
582+
pass
583+
584+
suite = unittest.defaultTestLoader.loadTestsFromTestCase(EmptyTest)
585+
runner = getRunner()
586+
runner.run(suite)
587+
588+
self.assertIn("\nNO TESTS RAN\n", runner.stream.getvalue())
589+
580590

581591
class TestModuleCleanUp(unittest.TestCase):
582592
def test_add_and_do_ModuleCleanup(self):

Lib/unittest/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .signals import installHandler
1010

1111
__unittest = True
12+
_NO_TESTS_EXITCODE = 5
1213

1314
MAIN_EXAMPLES = """\
1415
Examples:
@@ -279,6 +280,12 @@ def runTests(self):
279280
testRunner = self.testRunner
280281
self.result = testRunner.run(self.test)
281282
if self.exit:
282-
sys.exit(not self.result.wasSuccessful())
283+
if self.result.testsRun == 0:
284+
sys.exit(_NO_TESTS_EXITCODE)
285+
elif self.result.wasSuccessful():
286+
sys.exit(0)
287+
else:
288+
sys.exit(1)
289+
283290

284291
main = TestProgram

Lib/unittest/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,8 @@ def run(self, test):
274274
infos.append("failures=%d" % failed)
275275
if errored:
276276
infos.append("errors=%d" % errored)
277+
elif run == 0:
278+
self.stream.write("NO TESTS RAN")
277279
else:
278280
self.stream.write("OK")
279281
if skipped:

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,7 @@ Vlad Riscutia
15131513
Wes Rishel
15141514
Daniel Riti
15151515
Juan M. Bello Rivas
1516+
Stefano Rivera
15161517
Llandy Riveron Del Risco
15171518
Mohd Sanad Zaki Rizvi
15181519
Davide Rizzo
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :mod:`unittest` runner will now exit with status code 5 if no tests
2+
were run. It is common for test runner misconfiguration to fail to find
3+
any tests, this should be an error.

Parser/action_helpers.c

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -965,17 +965,43 @@ _PyPegen_check_legacy_stmt(Parser *p, expr_ty name) {
965965
return 0;
966966
}
967967

968-
expr_ty
969-
_PyPegen_check_fstring_conversion(Parser *p, Token* symbol, expr_ty conv) {
970-
if (symbol->lineno != conv->lineno || symbol->end_col_offset != conv->col_offset) {
968+
static ResultTokenWithMetadata *
969+
result_token_with_metadata(Parser *p, void *result, PyObject *metadata)
970+
{
971+
ResultTokenWithMetadata *res = _PyArena_Malloc(p->arena, sizeof(ResultTokenWithMetadata));
972+
if (res == NULL) {
973+
return NULL;
974+
}
975+
res->metadata = metadata;
976+
res->result = result;
977+
return res;
978+
}
979+
980+
ResultTokenWithMetadata *
981+
_PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv)
982+
{
983+
if (conv_token->lineno != conv->lineno || conv_token->end_col_offset != conv->col_offset) {
971984
return RAISE_SYNTAX_ERROR_KNOWN_RANGE(
972-
symbol, conv,
985+
conv_token, conv,
973986
"f-string: conversion type must come right after the exclamanation mark"
974987
);
975988
}
976-
return conv;
989+
return result_token_with_metadata(p, conv, conv_token->metadata);
977990
}
978991

992+
ResultTokenWithMetadata *
993+
_PyPegen_setup_full_format_spec(Parser *p, Token *colon, asdl_expr_seq *spec, int lineno, int col_offset,
994+
int end_lineno, int end_col_offset, PyArena *arena)
995+
{
996+
if (!spec) {
997+
return NULL;
998+
}
999+
expr_ty res = _PyAST_JoinedStr(spec, lineno, col_offset, end_lineno, end_col_offset, p->arena);
1000+
if (!res) {
1001+
return NULL;
1002+
}
1003+
return result_token_with_metadata(p, res, colon->metadata);
1004+
}
9791005

9801006
const char *
9811007
_PyPegen_get_expr_name(expr_ty e)
@@ -1197,27 +1223,6 @@ _PyPegen_nonparen_genexp_in_call(Parser *p, expr_ty args, asdl_comprehension_seq
11971223

11981224
// Fstring stuff
11991225

1200-
static expr_ty
1201-
decode_fstring_buffer(Parser *p, int lineno, int col_offset, int end_lineno,
1202-
int end_col_offset)
1203-
{
1204-
tokenizer_mode *tok_mode = &(p->tok->tok_mode_stack[p->tok->tok_mode_stack_index]);
1205-
assert(tok_mode->last_expr_buffer != NULL);
1206-
assert(tok_mode->last_expr_size >= 0 && tok_mode->last_expr_end >= 0);
1207-
1208-
PyObject *res = PyUnicode_DecodeUTF8(
1209-
tok_mode->last_expr_buffer,
1210-
tok_mode->last_expr_size - tok_mode->last_expr_end,
1211-
NULL
1212-
);
1213-
if (!res || _PyArena_AddPyObject(p->arena, res) < 0) {
1214-
Py_XDECREF(res);
1215-
return NULL;
1216-
}
1217-
1218-
return _PyAST_Constant(res, NULL, lineno, col_offset, end_lineno, end_col_offset, p->arena);
1219-
}
1220-
12211226
static expr_ty
12221227
_PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant) {
12231228
assert(PyUnicode_CheckExact(constant->v.Constant.value));
@@ -1386,19 +1391,20 @@ expr_ty _PyPegen_constant_from_string(Parser* p, Token* tok) {
13861391
return _PyAST_Constant(s, kind, tok->lineno, tok->col_offset, tok->end_lineno, tok->end_col_offset, p->arena);
13871392
}
13881393

1389-
expr_ty _PyPegen_formatted_value(Parser *p, expr_ty expression, Token *debug, expr_ty conversion,
1390-
expr_ty format, int lineno, int col_offset, int end_lineno, int end_col_offset,
1391-
PyArena *arena) {
1394+
expr_ty _PyPegen_formatted_value(Parser *p, expr_ty expression, Token *debug, ResultTokenWithMetadata *conversion,
1395+
ResultTokenWithMetadata *format, Token *closing_brace, int lineno, int col_offset,
1396+
int end_lineno, int end_col_offset, PyArena *arena) {
13921397
int conversion_val = -1;
13931398
if (conversion != NULL) {
1394-
assert(conversion->kind == Name_kind);
1395-
Py_UCS4 first = PyUnicode_READ_CHAR(conversion->v.Name.id, 0);
1399+
expr_ty conversion_expr = (expr_ty) conversion->result;
1400+
assert(conversion_expr->kind == Name_kind);
1401+
Py_UCS4 first = PyUnicode_READ_CHAR(conversion_expr->v.Name.id, 0);
13961402

1397-
if (PyUnicode_GET_LENGTH(conversion->v.Name.id) > 1 ||
1403+
if (PyUnicode_GET_LENGTH(conversion_expr->v.Name.id) > 1 ||
13981404
!(first == 's' || first == 'r' || first == 'a')) {
1399-
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conversion,
1405+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conversion_expr,
14001406
"f-string: invalid conversion character %R: expected 's', 'r', or 'a'",
1401-
conversion->v.Name.id);
1407+
conversion_expr->v.Name.id);
14021408
return NULL;
14031409
}
14041410

@@ -1410,30 +1416,34 @@ expr_ty _PyPegen_formatted_value(Parser *p, expr_ty expression, Token *debug, ex
14101416
}
14111417

14121418
expr_ty formatted_value = _PyAST_FormattedValue(
1413-
expression, conversion_val, format,
1419+
expression, conversion_val, format ? (expr_ty) format->result : NULL,
14141420
lineno, col_offset, end_lineno,
14151421
end_col_offset, arena
14161422
);
14171423

14181424
if (debug) {
14191425
/* Find the non whitespace token after the "=" */
14201426
int debug_end_line, debug_end_offset;
1427+
PyObject *debug_metadata;
14211428

14221429
if (conversion) {
1423-
debug_end_line = conversion->lineno;
1424-
debug_end_offset = conversion->col_offset;
1430+
debug_end_line = ((expr_ty) conversion->result)->lineno;
1431+
debug_end_offset = ((expr_ty) conversion->result)->col_offset;
1432+
debug_metadata = conversion->metadata;
14251433
}
14261434
else if (format) {
1427-
debug_end_line = format->lineno;
1428-
debug_end_offset = format->col_offset + 1; // HACK: ??
1435+
debug_end_line = ((expr_ty) format->result)->lineno;
1436+
debug_end_offset = ((expr_ty) format->result)->col_offset + 1;
1437+
debug_metadata = format->metadata;
14291438
}
14301439
else {
14311440
debug_end_line = end_lineno;
14321441
debug_end_offset = end_col_offset;
1442+
debug_metadata = closing_brace->metadata;
14331443
}
14341444

1435-
expr_ty debug_text = decode_fstring_buffer(p, lineno, col_offset + 1,
1436-
debug_end_line, debug_end_offset - 1);
1445+
expr_ty debug_text = _PyAST_Constant(debug_metadata, NULL, lineno, col_offset + 1, debug_end_line,
1446+
debug_end_offset - 1, p->arena);
14371447
if (!debug_text) {
14381448
return NULL;
14391449
}

0 commit comments

Comments
 (0)