diff --git a/Doc/builtins/functions.rst b/Doc/builtins/functions.rst index ece666030db981..8d9160c0897d3e 100644 --- a/Doc/builtins/functions.rst +++ b/Doc/builtins/functions.rst @@ -385,13 +385,13 @@ are always available. They are listed here in alphabetical order. by module name. This function raises :exc:`SyntaxError` if the compiled source is invalid, - including a *source* containing a null character or that cannot be decoded; - :exc:`ValueError` if *mode* or *flags* is invalid, - or if a string *source* contains surrogate characters; - :exc:`MemoryError` or :exc:`RecursionError` if *source* is too complex - to parse or compile, - for example an expression with many thousands of nested operators; - and :exc:`OverflowError` if *source* is too large. + including a *source* containing a null or surrogate character, + that cannot be decoded, + that is too complex to parse or compile, + for example an expression with many thousands of nested operators, + or that is too large; + :exc:`RecursionError` if an AST object *source* is too deeply nested; + and :exc:`ValueError` if *mode* or *flags* is invalid. If you want to parse Python code into its AST representation, see :func:`ast.parse`. @@ -434,6 +434,13 @@ are always available. They are listed here in alphabetical order. .. versionchanged:: 3.15 Added the *module* parameter. + .. versionchanged:: next + :exc:`SyntaxError` is raised instead of :exc:`ValueError` when surrogate + characters are encountered in *source*, + instead of :exc:`MemoryError` or :exc:`RecursionError` + when *source* is too complex, + and instead of :exc:`OverflowError` when *source* is too large. + .. class:: complex(number=0, /) complex(string, /) diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index e30dadc9733d37..424de8044e616f 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -2338,9 +2338,8 @@ and classes for traversing abstract syntax trees: It is possible to crash the Python interpreter due to stack depth limitations in Python's AST compiler. - It can raise :exc:`ValueError`, :exc:`TypeError`, :exc:`SyntaxError`, - :exc:`MemoryError` and :exc:`RecursionError` depending on the malformed - input. + It can raise :exc:`ValueError`, :exc:`TypeError` or :exc:`SyntaxError` + depending on the malformed input. .. versionchanged:: 3.2 Now allows bytes and set literals. diff --git a/Doc/library/code.rst b/Doc/library/code.rst index 71eb3e8646cd01..70a81b178f26b9 100644 --- a/Doc/library/code.rst +++ b/Doc/library/code.rst @@ -74,9 +74,7 @@ build applications which provide an interactive interpreter prompt. Returns a code object (the same as ``compile(source, filename, symbol)``) if the command is complete and valid; ``None`` if the command is incomplete; raises - :exc:`SyntaxError` if the command is complete and contains a syntax error, or - raises :exc:`OverflowError` or :exc:`ValueError` if the command contains an - invalid literal. + :exc:`SyntaxError` if the command is complete and invalid. .. _interpreter-objects: @@ -91,8 +89,8 @@ Interactive Interpreter Objects :func:`compile_command`; the default for *filename* is ``''``, and for *symbol* is ``'single'``. One of several things can happen: - * The input is incorrect; :func:`compile_command` raised an exception - (usually :exc:`SyntaxError`). A syntax traceback will be + * The input is incorrect; :func:`compile_command` raised + :exc:`SyntaxError`. A syntax traceback will be printed by calling the :meth:`showsyntaxerror` method. :meth:`runsource` returns ``False``. diff --git a/Doc/library/codeop.rst b/Doc/library/codeop.rst index 622e57d2ee63db..ac50a0e2d981e8 100644 --- a/Doc/library/codeop.rst +++ b/Doc/library/codeop.rst @@ -35,9 +35,7 @@ To do just the former: ``''``. Returns ``None`` if *source* is *not* valid Python code, but is a prefix of valid Python code. - If there is a problem with *source*, an exception will be raised. - :exc:`SyntaxError` is raised if there is invalid Python syntax, and - :exc:`OverflowError` or :exc:`ValueError` if there is an invalid literal. + If there is a problem with *source*, :exc:`SyntaxError` is raised. The *symbol* argument determines whether *source* is compiled as a statement (``'single'``, the default), as a sequence of :term:`statement` (``'exec'``) or diff --git a/Include/internal/pycore_compile.h b/Include/internal/pycore_compile.h index 31738f2074dbbf..e6b752c70412bb 100644 --- a/Include/internal/pycore_compile.h +++ b/Include/internal/pycore_compile.h @@ -36,6 +36,8 @@ PyAPI_FUNC(PyCodeObject*) _PyAST_Compile( PyObject *module); /* AST preprocessing */ +extern void _PyCompile_CheckRecursionError(void); + extern int _PyCompile_AstPreprocess( struct _mod *mod, PyObject *filename, diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h index d333eb2ccf7c41..4743436ed77cb5 100644 --- a/Include/internal/pycore_pythonrun.h +++ b/Include/internal/pycore_pythonrun.h @@ -25,6 +25,7 @@ extern const char* _Py_SourceAsString( PyObject *cmd, const char *funcname, const char *what, + PyObject *filename, PyCompilerFlags *cf, PyObject **cmd_copy); diff --git a/Lib/code.py b/Lib/code.py index f658049197e1b7..21fec4c6343679 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -44,7 +44,7 @@ def runsource(self, source, filename="", symbol="single"): One of several things can happen: 1) The input is incorrect; compile_command() raised an - exception (usually SyntaxError). A syntax traceback will be + exception (SyntaxError). A syntax traceback will be printed by calling the showsyntaxerror() method. 2) The input is incomplete, and more input is required; diff --git a/Lib/codeop.py b/Lib/codeop.py index 40e88423119bc4..a314029b42870e 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -6,9 +6,7 @@ - Return code object if the command is complete and valid - Return None if the command is incomplete -- Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). +- Raise SyntaxError if the command is a syntax error. The two interfaces are: @@ -95,9 +93,7 @@ def compile_command(source, filename="", symbol="single", flags=0): - Return a code object if the command is complete and valid - Return None if the command is incomplete - - Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). + - Raise SyntaxError if the command is a syntax error. """ return _maybe_compile(_compile, source, filename, symbol, flags) @@ -147,8 +143,6 @@ def __call__(self, source, filename="", symbol="single"): - Return a code object if the command is complete and valid - Return None if the command is incomplete - - Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). + - Raise SyntaxError if the command is a syntax error. """ return _maybe_compile(self.compiler, source, filename, symbol, flags=self.compiler.flags) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index bb282eba5d41ee..fa79bffee0d299 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -1126,7 +1126,8 @@ def check_limit(prefix, repeated): broken = prefix + repeated * crash_depth details = "Compiling ({!r} + {!r} * {})".format( prefix, repeated, crash_depth) - with self.assertRaises(RecursionError, msg=details): + with self.assertRaisesRegex(SyntaxError, "too complex to compile", + msg=details): with support.infinite_recursion(): ast.parse(broken) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 294355698c8683..4a9a1a18676384 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -446,6 +446,13 @@ def test_compile(self): mode='eval', source='0', filename='tmp') compile('print("\xe5")\n', '', 'exec') self.assertRaises(SyntaxError, compile, chr(0), 'f', 'exec') + with self.assertRaises(SyntaxError) as cm: + compile("x = 1\ny = 'ab\udc80cd'\n", 'f', 'exec') + self.assertEqual((cm.exception.filename, cm.exception.lineno, + cm.exception.offset, cm.exception.text), + ('f', 2, 8, "y = 'ab\udc80cd'")) + self.assertRaises(SyntaxError, eval, '\udc80') + self.assertRaises(SyntaxError, exec, '\udc80') self.assertRaises(ValueError, compile, str('a = 1'), 'f', 'bad') # test the optimize argument diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py index c0c3454a97fbd8..ae019a84a54b06 100644 --- a/Lib/test/test_code_module.py +++ b/Lib/test/test_code_module.py @@ -132,23 +132,29 @@ def test_unicode_error(self): self.console.interact() output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) output = output[output.index('(InteractiveConsole)'):] - output = output[output.index('\n') + 1:] - self.assertStartsWith(output, 'UnicodeEncodeError: ') - self.assertIs(self.sysmod.last_type, UnicodeEncodeError) - self.assertIs(type(self.sysmod.last_value), UnicodeEncodeError) + output = output[:output.index('\nnow exiting')] + self.assertEqual(output.splitlines()[1:], [ + ' File "", line 1', + " '\ud800'", + ' ^', + 'SyntaxError: source code string cannot contain surrogate characters']) + self.assertIs(self.sysmod.last_type, SyntaxError) + self.assertIs(type(self.sysmod.last_value), SyntaxError) self.assertIsNone(self.sysmod.last_traceback) self.assertIsNone(self.sysmod.last_value.__traceback__) self.assertIs(self.sysmod.last_exc, self.sysmod.last_value) def test_compile_error(self): # Any error raised by compile() must be reported (gh-69919). - self.infunc.side_effect = ['-' * 100_000 + '1', EOFError('Finished')] - self.console.interact() + self.infunc.side_effect = ['1', EOFError('Finished')] + with mock.patch.object(self.console, 'compile', + side_effect=MemoryError('spam')): + self.console.interact() output = ''.join(''.join(call[1]) for call in self.stderr.method_calls) output = output[output.index('(InteractiveConsole)'):] output = output[output.index('\n') + 1:] - self.assertRegex(output, r'^(MemoryError|RecursionError): ') - self.assertIn(self.sysmod.last_type, (MemoryError, RecursionError)) + self.assertRegex(output, r'^MemoryError: spam\n') + self.assertIs(self.sysmod.last_type, MemoryError) self.assertIs(self.sysmod.last_exc, self.sysmod.last_value) def test_sysexcepthook(self): diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 553ac70d83a802..be86d5818042f6 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -763,7 +763,8 @@ def check_limit(prefix, repeated, mode="single"): compile(expect_ok, '', mode) broken = prefix + repeated * crash_depth details = f"Compiling ({prefix!r} + {repeated!r} * {crash_depth})" - with self.assertRaises(RecursionError, msg=details): + with self.assertRaisesRegex(SyntaxError, "too complex to compile", + msg=details): compile(broken, '', mode) check_limit("a", "()") diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index 673827608abc47..1a3dbb5f524d92 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -135,12 +135,13 @@ def test_runsource_shows_syntax_error_for_failed_compilation(self): def test_runsource_compile_error(self): # Any error raised by compile() is reported (gh-69919). console = InteractiveColoredConsole() - source = '-' * 100_000 + '1' f = io.StringIO() - with contextlib.redirect_stderr(f): - result = console.runsource(source) + with (contextlib.redirect_stderr(f), + patch.object(console.compile, 'compiler', + side_effect=MemoryError('spam'))): + result = console.runsource('1') self.assertFalse(result) - self.assertRegex(f.getvalue(), r'^(MemoryError|RecursionError): ') + self.assertRegex(f.getvalue(), r'^MemoryError: spam$') def test_runsource_survives_null_bytes(self): console = InteractiveColoredConsole() @@ -195,9 +196,10 @@ def test_invalid_syntax_single_line(self): def test_compile_error_single_line(self): namespace = {} - code = '-' * 100_000 + '1' # MemoryError or RecursionError + code = "1" console = InteractiveColoredConsole(namespace, filename="") - self.assertFalse(_more_lines(console, code)) + with patch.object(console, 'compile', side_effect=MemoryError): + self.assertFalse(_more_lines(console, code)) def test_empty_line(self): namespace = {} diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 2001b70739fd26..bf3e34c69dd18f 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -3560,7 +3560,7 @@ def test_error_on_parser_stack_overflow(self): source = "-" * 100000 + "4" for mode in ["exec", "eval", "single"]: with self.subTest(mode=mode): - with self.assertRaisesRegex(MemoryError, r"too complex"): + with self.assertRaisesRegex(SyntaxError, r"too complex to parse"): compile(source, "", mode) @support.cpython_only diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-15-19-23-52.gh-issue-69919.Xx1A2D.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-15-19-23-52.gh-issue-69919.Xx1A2D.rst new file mode 100644 index 00000000000000..1bd43715f49c8d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-15-19-23-52.gh-issue-69919.Xx1A2D.rst @@ -0,0 +1,5 @@ +:func:`compile`, :func:`exec`, :func:`eval` and :func:`ast.parse` now raise +:exc:`SyntaxError` instead of :exc:`ValueError` if the source string contains +surrogate characters, instead of :exc:`MemoryError` or :exc:`RecursionError` +if the source is too complex to parse or compile, and instead of +:exc:`OverflowError` if the source is too large. diff --git a/Modules/symtablemodule.c b/Modules/symtablemodule.c index 3028158fc917e1..947f340cdb0506 100644 --- a/Modules/symtablemodule.c +++ b/Modules/symtablemodule.c @@ -92,7 +92,7 @@ _symtable_symtable_impl(PyObject *module, PyObject *source, cf.cf_flags = PyCF_SOURCE_IS_UTF8; const char *str = _Py_SourceAsString(source, "symtable", "string, bytes or AST", - &cf, &source_copy); + filename, &cf, &source_copy); if (str == NULL) { return NULL; } diff --git a/Parser/pegen.c b/Parser/pegen.c index 5edf5542c7a8f9..21515a9564da42 100644 --- a/Parser/pegen.c +++ b/Parser/pegen.c @@ -2,6 +2,7 @@ #include "pycore_ast.h" // _PyAST_Validate(), #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_parser.h" // _PYPEGEN_NSTATISTICS +#include "pycore_compile.h" // _PyCompile_CheckRecursionError() #include "pycore_pyerrors.h" // PyExc_IncompleteInputError #include "pycore_runtime.h" // _PyRuntime #include "pycore_unicodeobject.h" // _PyUnicode_InternImmortal @@ -1111,6 +1112,7 @@ _PyPegen_run_parser(Parser *p) p->start_rule == Py_eval_input) { if (!_PyAST_Validate(res)) { + _PyCompile_CheckRecursionError(); return NULL; } } diff --git a/Parser/pegen_errors.c b/Parser/pegen_errors.c index 7841f01b612915..f4ee83e98e7509 100644 --- a/Parser/pegen_errors.c +++ b/Parser/pegen_errors.c @@ -68,9 +68,8 @@ _Pypegen_tokenizer_error(Parser *p) break; } case E_COLUMNOVERFLOW: - PyErr_SetString(PyExc_OverflowError, - "Parser column offset overflow - source line is too big"); - return -1; + msg = "source line is too long"; + break; default: msg = "unknown parsing error"; } @@ -391,6 +390,5 @@ void _Pypegen_stack_overflow(Parser *p) { p->error_indicator = 1; - PyErr_SetString(PyExc_MemoryError, - "Parser stack overflowed - Python source too complex to parse"); + RAISE_SYNTAX_ERROR("Python source too complex to parse"); } diff --git a/Parser/string_parser.c b/Parser/string_parser.c index 20f523a262b225..9ec8fdcd1b63a9 100644 --- a/Parser/string_parser.c +++ b/Parser/string_parser.c @@ -291,7 +291,7 @@ _PyPegen_parse_string(Parser *p, Token *t) assert(len >= 1); if (len > INT_MAX) { - PyErr_SetString(PyExc_OverflowError, "string to parse is too long"); + RAISE_SYNTAX_ERROR_KNOWN_LOCATION(t, "string literal is too long"); return NULL; } if (s[--len] != quote) { diff --git a/Parser/tokenizer/source.c b/Parser/tokenizer/source.c index 2f2aaf2589246d..51a131f7bbb779 100644 --- a/Parser/tokenizer/source.c +++ b/Parser/tokenizer/source.c @@ -146,7 +146,7 @@ validate_line(const _PyTok_SourceText *source, const char *bytes, } if (source->nlines == INT_MAX || (source->nlines == INT_MAX - 1 && newline != NULL)) { - PyErr_SetString(PyExc_OverflowError, "too many tokenizer source lines"); + PyErr_SetString(PyExc_SyntaxError, "too many lines in source"); return -1; } return 0; diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 965cf20fe61778..13d5c593722147 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -949,7 +949,8 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename, goto finally; } - str = _Py_SourceAsString(source, "compile", "string, bytes or AST", &cf, &source_copy); + str = _Py_SourceAsString(source, "compile", "string, bytes or AST", + filename, &cf, &source_copy); if (str == NULL) goto error; @@ -1116,7 +1117,8 @@ builtin_eval_impl(PyObject *module, PyObject *source, PyObject *globals, else { PyCompilerFlags cf = _PyCompilerFlags_INIT; cf.cf_flags = PyCF_SOURCE_IS_UTF8; - str = _Py_SourceAsString(source, "eval", "string, bytes or code", &cf, &source_copy); + str = _Py_SourceAsString(source, "eval", "string, bytes or code", + NULL, &cf, &source_copy); if (str == NULL) goto error; @@ -1286,7 +1288,7 @@ builtin_exec_impl(PyObject *module, PyObject *source, PyObject *globals, PyCompilerFlags cf = _PyCompilerFlags_INIT; cf.cf_flags = PyCF_SOURCE_IS_UTF8; str = _Py_SourceAsString(source, "exec", - "string, bytes or code", &cf, + "string, bytes or code", NULL, &cf, &source_copy); if (str == NULL) goto error; diff --git a/Python/compile.c b/Python/compile.c index ee29f7a9a5d589..c0fb39ce8b5e07 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -1526,6 +1526,18 @@ _PyCompile_OptimizeAndAssemble(compiler *c, int addNone) return optimize_and_assemble_code_unit(u, const_cache, code_flags, filename); } +/* Replace a RecursionError raised while processing too deeply nested + source with a SyntaxError, as the parser raises for such source. */ +void +_PyCompile_CheckRecursionError(void) +{ + if (PyErr_ExceptionMatches(PyExc_RecursionError)) { + PyErr_Clear(); + PyErr_SetString(PyExc_SyntaxError, + "Python source too complex to compile"); + } +} + PyCodeObject * _PyAST_Compile(mod_ty mod, PyObject *filename, PyCompilerFlags *pflags, int optimize, PyArena *arena, PyObject *module) @@ -1533,12 +1545,16 @@ _PyAST_Compile(mod_ty mod, PyObject *filename, PyCompilerFlags *pflags, assert(!PyErr_Occurred()); compiler *c = new_compiler(mod, filename, pflags, optimize, arena, module); if (c == NULL) { + _PyCompile_CheckRecursionError(); return NULL; } PyCodeObject *co = compiler_mod(c, mod); compiler_free(c); assert(co || PyErr_Occurred()); + if (co == NULL) { + _PyCompile_CheckRecursionError(); + } return co; } @@ -1558,6 +1574,7 @@ _PyCompile_AstPreprocess(mod_ty mod, PyObject *filename, PyCompilerFlags *cf, if (!_PyAST_Preprocess(mod, arena, filename, optimize, flags, no_const_folding, 0, module)) { + _PyCompile_CheckRecursionError(); return -1; } return 0; diff --git a/Python/crossinterp.c b/Python/crossinterp.c index ed77c1be646e27..aa7b6359b00f0a 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -943,7 +943,7 @@ get_script_xidata(PyThreadState *tstate, PyObject *obj, int pure, PyCompilerFlags cf = _PyCompilerFlags_INIT; cf.cf_flags = PyCF_SOURCE_IS_UTF8; PyObject *ref = NULL; - const char *script = _Py_SourceAsString(obj, "???", "???", &cf, &ref); + const char *script = _Py_SourceAsString(obj, "???", "???", NULL, &cf, &ref); if (script == NULL) { if (!_PyObject_SupportedAsScript(obj)) { // We discard the raised exception. diff --git a/Python/pythonrun.c b/Python/pythonrun.c index 049eaea4994ac4..f39c32201a5cba 100644 --- a/Python/pythonrun.c +++ b/Python/pythonrun.c @@ -1575,26 +1575,30 @@ _Py_CompileString(const char *str, PyObject *filename, int start, if (arena == NULL) return NULL; + PyObject *result = NULL; mod = _PyParser_ASTFromString(str, filename, start, flags, arena, module); if (mod == NULL) { - _PyArena_Free(arena); - return NULL; + goto done; } if (flags && (flags->cf_flags & PyCF_ONLY_AST)) { int syntax_check_only = ((flags->cf_flags & PyCF_OPTIMIZED_AST) == PyCF_ONLY_AST); /* unoptiomized AST */ if (_PyCompile_AstPreprocess(mod, filename, flags, optimize, arena, syntax_check_only, module) < 0) { - _PyArena_Free(arena); - return NULL; + goto done; } - PyObject *result = PyAST_mod2obj(mod); - _PyArena_Free(arena); - return result; + result = PyAST_mod2obj(mod); + } + else { + co = _PyAST_Compile(mod, filename, flags, optimize, arena, module); + result = (PyObject *)co; } - co = _PyAST_Compile(mod, filename, flags, optimize, arena, module); +done: _PyArena_Free(arena); - return (PyObject *)co; + if (result == NULL) { + _PyCompile_CheckRecursionError(); + } + return result; } PyObject * @@ -1630,8 +1634,48 @@ _PyObject_SupportedAsScript(PyObject *cmd) } } +/* Raise a SyntaxError for the character at index pos of the source string. */ +static void +source_syntax_error(PyObject *source, PyObject *filename, Py_ssize_t pos, + const char *msg) +{ + Py_ssize_t len = PyUnicode_GET_LENGTH(source); + Py_ssize_t lineno = 1; + Py_ssize_t linestart = 0; + for (Py_ssize_t i = 0; i < pos; i++) { + if (PyUnicode_READ_CHAR(source, i) == '\n') { + lineno++; + linestart = i + 1; + } + } + Py_ssize_t lineend = PyUnicode_FindChar(source, '\n', pos, len, 1); + if (lineend < 0) { + lineend = len; + } + PyObject *text = PyUnicode_Substring(source, linestart, lineend); + if (text == NULL) { + return; + } + Py_ssize_t offset = pos - linestart + 1; + PyObject *args; + if (filename != NULL) { + args = Py_BuildValue("s(OnnNnn)", msg, filename, + lineno, offset, text, lineno, offset + 1); + } + else { + args = Py_BuildValue("s(snnNnn)", msg, "", + lineno, offset, text, lineno, offset + 1); + } + if (args == NULL) { + return; + } + PyErr_SetObject(PyExc_SyntaxError, args); + Py_DECREF(args); +} + const char * -_Py_SourceAsString(PyObject *cmd, const char *funcname, const char *what, PyCompilerFlags *cf, PyObject **cmd_copy) +_Py_SourceAsString(PyObject *cmd, const char *funcname, const char *what, + PyObject *filename, PyCompilerFlags *cf, PyObject **cmd_copy) { const char *str; Py_ssize_t size; @@ -1641,8 +1685,18 @@ _Py_SourceAsString(PyObject *cmd, const char *funcname, const char *what, PyComp if (PyUnicode_Check(cmd)) { cf->cf_flags |= PyCF_IGNORE_COOKIE; str = PyUnicode_AsUTF8AndSize(cmd, &size); - if (str == NULL) + if (str == NULL) { + if (PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) { + PyObject *exc = PyErr_GetRaisedException(); + Py_ssize_t pos; + if (PyUnicodeEncodeError_GetStart(exc, &pos) == 0) { + source_syntax_error(cmd, filename, pos, + "source code string cannot contain surrogate characters"); + } + Py_DECREF(exc); + } return NULL; + } } else if (PyBytes_Check(cmd)) { str = PyBytes_AS_STRING(cmd);