From a4056c8f9c2d9970d39e3cb6bffb255cd4b8a42c Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Thu, 15 Jun 2023 15:45:13 -0700 Subject: [PATCH 1/9] GH-105588: Add missing error checks to some obj2ast_* converters (GH-105589) --- Lib/test/test_ast.py | 27 +++++++++++++++++++ ...-06-09-11-19-51.gh-issue-105588.Y5ovpY.rst | 2 ++ Parser/asdl_c.py | 1 + Python/Python-ast.c | 7 +++++ 4 files changed, 37 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-06-09-11-19-51.gh-issue-105588.Y5ovpY.rst diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py index ffd082ec11806a..a03fa4c7187b05 100644 --- a/Lib/test/test_ast.py +++ b/Lib/test/test_ast.py @@ -3,6 +3,7 @@ import dis import enum import os +import re import sys import textwrap import types @@ -1110,6 +1111,32 @@ def test_null_bytes(self): msg="source code string cannot contain null bytes"): ast.parse("a\0b") + def assert_none_check(self, node: type[ast.AST], attr: str, source: str) -> None: + with self.subTest(f"{node.__name__}.{attr}"): + tree = ast.parse(source) + found = 0 + for child in ast.walk(tree): + if isinstance(child, node): + setattr(child, attr, None) + found += 1 + self.assertEqual(found, 1) + e = re.escape(f"field '{attr}' is required for {node.__name__}") + with self.assertRaisesRegex(ValueError, f"^{e}$"): + compile(tree, "", "exec") + + def test_none_checks(self) -> None: + tests = [ + (ast.alias, "name", "import spam as SPAM"), + (ast.arg, "arg", "def spam(SPAM): spam"), + (ast.comprehension, "target", "[spam for SPAM in spam]"), + (ast.comprehension, "iter", "[spam for spam in SPAM]"), + (ast.keyword, "value", "spam(**SPAM)"), + (ast.match_case, "pattern", "match spam:\n case SPAM: spam"), + (ast.withitem, "context_expr", "with SPAM: spam"), + ] + for node, attr, source in tests: + self.assert_none_check(node, attr, source) + class ASTHelpers_Test(unittest.TestCase): maxDiff = None diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-06-09-11-19-51.gh-issue-105588.Y5ovpY.rst b/Misc/NEWS.d/next/Core and Builtins/2023-06-09-11-19-51.gh-issue-105588.Y5ovpY.rst new file mode 100644 index 00000000000000..3981dad7a49dfb --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-06-09-11-19-51.gh-issue-105588.Y5ovpY.rst @@ -0,0 +1,2 @@ +Fix an issue that could result in crashes when compiling malformed +:mod:`ast` nodes. diff --git a/Parser/asdl_c.py b/Parser/asdl_c.py index cb312796f89e04..d4763ea260a5a2 100755 --- a/Parser/asdl_c.py +++ b/Parser/asdl_c.py @@ -601,6 +601,7 @@ def visitProduct(self, prod, name): args = [f.name for f in prod.fields] args.extend([a.name for a in prod.attributes]) self.emit("*out = %s(%s);" % (ast_func_name(name), self.buildArgs(args)), 1) + self.emit("if (*out == NULL) goto failed;", 1) self.emit("return 0;", 1) self.emit("failed:", 0) self.emit("Py_XDECREF(tmp);", 1) diff --git a/Python/Python-ast.c b/Python/Python-ast.c index 84bce59e271471..1ffb8354e3a1b1 100644 --- a/Python/Python-ast.c +++ b/Python/Python-ast.c @@ -10834,6 +10834,7 @@ obj2ast_comprehension(struct ast_state *state, PyObject* obj, comprehension_ty* Py_CLEAR(tmp); } *out = _PyAST_comprehension(target, iter, ifs, is_async, arena); + if (*out == NULL) goto failed; return 0; failed: Py_XDECREF(tmp); @@ -11258,6 +11259,7 @@ obj2ast_arguments(struct ast_state *state, PyObject* obj, arguments_ty* out, } *out = _PyAST_arguments(posonlyargs, args, vararg, kwonlyargs, kw_defaults, kwarg, defaults, arena); + if (*out == NULL) goto failed; return 0; failed: Py_XDECREF(tmp); @@ -11397,6 +11399,7 @@ obj2ast_arg(struct ast_state *state, PyObject* obj, arg_ty* out, PyArena* arena) } *out = _PyAST_arg(arg, annotation, type_comment, lineno, col_offset, end_lineno, end_col_offset, arena); + if (*out == NULL) goto failed; return 0; failed: Py_XDECREF(tmp); @@ -11519,6 +11522,7 @@ obj2ast_keyword(struct ast_state *state, PyObject* obj, keyword_ty* out, } *out = _PyAST_keyword(arg, value, lineno, col_offset, end_lineno, end_col_offset, arena); + if (*out == NULL) goto failed; return 0; failed: Py_XDECREF(tmp); @@ -11641,6 +11645,7 @@ obj2ast_alias(struct ast_state *state, PyObject* obj, alias_ty* out, PyArena* } *out = _PyAST_alias(name, asname, lineno, col_offset, end_lineno, end_col_offset, arena); + if (*out == NULL) goto failed; return 0; failed: Py_XDECREF(tmp); @@ -11690,6 +11695,7 @@ obj2ast_withitem(struct ast_state *state, PyObject* obj, withitem_ty* out, Py_CLEAR(tmp); } *out = _PyAST_withitem(context_expr, optional_vars, arena); + if (*out == NULL) goto failed; return 0; failed: Py_XDECREF(tmp); @@ -11778,6 +11784,7 @@ obj2ast_match_case(struct ast_state *state, PyObject* obj, match_case_ty* out, Py_CLEAR(tmp); } *out = _PyAST_match_case(pattern, guard, body, arena); + if (*out == NULL) goto failed; return 0; failed: Py_XDECREF(tmp); From 25a64fd28aaaaf2d21fae23212a0554c24fc7b20 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Thu, 15 Jun 2023 16:34:42 -0700 Subject: [PATCH 2/9] GH-103124: Multiline statement support for pdb (GH-103125) --- Lib/pdb.py | 26 ++++++++++++++- Lib/test/test_pdb.py | 33 +++++++++++++++++-- ...-04-09-03-53-02.gh-issue-103124.JspiNN.rst | 1 + 3 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-04-09-03-53-02.gh-issue-103124.JspiNN.rst diff --git a/Lib/pdb.py b/Lib/pdb.py index 1af511788749db..3db3e6a5be1a7b 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -76,6 +76,7 @@ import dis import code import glob +import codeop import pprint import signal import inspect @@ -444,7 +445,30 @@ def default(self, line): locals = self.curframe_locals globals = self.curframe.f_globals try: - code = compile(line + '\n', '', 'single') + if (code := codeop.compile_command(line + '\n', '', 'single')) is None: + # Multi-line mode + buffer = line + continue_prompt = "... " + while (code := codeop.compile_command(buffer, '', 'single')) is None: + if self.use_rawinput: + try: + line = input(continue_prompt) + except (EOFError, KeyboardInterrupt): + self.lastcmd = "" + print('\n') + return + else: + self.stdout.write(continue_prompt) + self.stdout.flush() + line = self.stdin.readline() + if not len(line): + self.lastcmd = "" + self.stdout.write('\n') + self.stdout.flush() + return + else: + line = line.rstrip('\r\n') + buffer += '\n' + line save_stdout = sys.stdout save_stdin = sys.stdin save_displayhook = sys.displayhook diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 277400e152476f..a66953557e52dc 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -389,7 +389,7 @@ def test_pdb_breakpoints_preserved_across_interactive_sessions(): 1 breakpoint keep yes at ...test_pdb.py:... 2 breakpoint keep yes at ...test_pdb.py:... (Pdb) break pdb.find_function - Breakpoint 3 at ...pdb.py:97 + Breakpoint 3 at ...pdb.py:... (Pdb) break Num Type Disp Enb Where 1 breakpoint keep yes at ...test_pdb.py:... @@ -1589,6 +1589,32 @@ def test_pdb_next_command_subiterator(): (Pdb) continue """ +def test_pdb_multiline_statement(): + """Test for multiline statement + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True, readrc=False).set_trace() + ... pass + + >>> with PdbTestInput([ # doctest: +NORMALIZE_WHITESPACE + ... 'def f(x):', + ... ' return x * 2', + ... '', + ... 'f(2)', + ... 'c' + ... ]): + ... test_function() + > (3)test_function() + -> pass + (Pdb) def f(x): + ... return x * 2 + ... + (Pdb) f(2) + 4 + (Pdb) c + """ + + def test_pdb_issue_20766(): """Test for reference leaks when the SIGINT handler is set. @@ -2362,7 +2388,7 @@ def test_relative_imports_on_plain_module(self): def test_errors_in_command(self): commands = "\n".join([ - 'print(', + 'print(]', 'debug print(', 'debug doesnotexist', 'c', @@ -2371,7 +2397,8 @@ def test_errors_in_command(self): self.assertEqual(stdout.splitlines()[1:], [ '-> pass', - '(Pdb) *** SyntaxError: \'(\' was never closed', + "(Pdb) *** SyntaxError: closing parenthesis ']' does not match opening " + "parenthesis '('", '(Pdb) ENTERING RECURSIVE DEBUGGER', '*** SyntaxError: \'(\' was never closed', diff --git a/Misc/NEWS.d/next/Library/2023-04-09-03-53-02.gh-issue-103124.JspiNN.rst b/Misc/NEWS.d/next/Library/2023-04-09-03-53-02.gh-issue-103124.JspiNN.rst new file mode 100644 index 00000000000000..022524b369aa10 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-09-03-53-02.gh-issue-103124.JspiNN.rst @@ -0,0 +1 @@ +Added multiline statement support for :mod:`pdb` From 486b52a3158e0f64fc54efdfa34ed5437b3619f2 Mon Sep 17 00:00:00 2001 From: Alex Doe Date: Thu, 15 Jun 2023 18:04:57 -0600 Subject: [PATCH 3/9] bpo-44530: Document the change in MAKE_FUNCTION behavior (#93189) * bpo-44530: Document the change in MAKE_FUNCTION behavior Fixes dis module documentation for MAKE_FUNCTION due to https://github.com/python/cpython/commit/2f180ce2cb6e6a7e3c517495e0f4873d6aaf5f2f (bpo-44530, released as part of 3.11) removes the qualified name at TOS --- Doc/library/dis.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst index 714dbc38f168ae..c6f08affcbf2d0 100644 --- a/Doc/library/dis.rst +++ b/Doc/library/dis.rst @@ -1431,12 +1431,15 @@ iterations of the loop. * ``0x02`` a dictionary of keyword-only parameters' default values * ``0x04`` a tuple of strings containing parameters' annotations * ``0x08`` a tuple containing cells for free variables, making a closure - * the code associated with the function (at ``STACK[-2]``) - * the :term:`qualified name` of the function (at ``STACK[-1]``) + * the code associated with the function (at ``STACK[-1]``) .. versionchanged:: 3.10 Flag value ``0x04`` is a tuple of strings instead of dictionary + .. versionchanged:: 3.11 + Qualified name at ``STACK[-1]`` was removed. + + .. opcode:: BUILD_SLICE (argc) .. index:: pair: built-in function; slice From 1af8251d9ec2f18e131c19ccf776fb9ec132c7a8 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 16 Jun 2023 03:58:40 +0300 Subject: [PATCH 4/9] gh-105433: Add `pickle` tests for PEP695 (#105443) --- Lib/test/test_type_aliases.py | 69 +++++++++++++++++++++++++++++++++-- Lib/test/test_type_params.py | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index a3067e521e023e..b9b24448e83d8e 100644 --- a/Lib/test/test_type_aliases.py +++ b/Lib/test/test_type_aliases.py @@ -228,8 +228,69 @@ def test_module(self): self.assertEqual(mod_generics_cache.OldStyle.__module__, mod_generics_cache.__name__) + +# All these type aliases are used for pickling tests: +T = TypeVar('T') +type SimpleAlias = int +type RecursiveAlias = dict[str, RecursiveAlias] +type GenericAlias[X] = list[X] +type GenericAliasMultipleTypes[X, Y] = dict[X, Y] +type RecursiveGenericAlias[X] = dict[str, RecursiveAlias[X]] +type BoundGenericAlias[X: int] = set[X] +type ConstrainedGenericAlias[LongName: (str, bytes)] = list[LongName] +type AllTypesAlias[A, *B, **C] = Callable[C, A] | tuple[*B] + + +class TypeAliasPickleTest(unittest.TestCase): def test_pickling(self): - pickled = pickle.dumps(mod_generics_cache.Alias) - self.assertIs(pickle.loads(pickled), mod_generics_cache.Alias) - pickled = pickle.dumps(mod_generics_cache.OldStyle) - self.assertIs(pickle.loads(pickled), mod_generics_cache.OldStyle) + things_to_test = [ + SimpleAlias, + RecursiveAlias, + + GenericAlias, + GenericAlias[T], + GenericAlias[int], + + GenericAliasMultipleTypes, + GenericAliasMultipleTypes[str, T], + GenericAliasMultipleTypes[T, str], + GenericAliasMultipleTypes[int, str], + + RecursiveGenericAlias, + RecursiveGenericAlias[T], + RecursiveGenericAlias[int], + + BoundGenericAlias, + BoundGenericAlias[int], + BoundGenericAlias[T], + + ConstrainedGenericAlias, + ConstrainedGenericAlias[str], + ConstrainedGenericAlias[T], + + AllTypesAlias, + AllTypesAlias[int, str, T, [T, object]], + + # Other modules: + mod_generics_cache.Alias, + mod_generics_cache.OldStyle, + ] + for thing in things_to_test: + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(thing=thing, proto=proto): + pickled = pickle.dumps(thing, protocol=proto) + self.assertEqual(pickle.loads(pickled), thing) + + type ClassLevel = str + + def test_pickling_local(self): + type A = int + things_to_test = [ + self.ClassLevel, + A, + ] + for thing in things_to_test: + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(thing=thing, proto=proto): + with self.assertRaises(pickle.PickleError): + pickle.dumps(thing, protocol=proto) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 6475df6f5bba43..3026cc22476619 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -2,6 +2,7 @@ import textwrap import types import unittest +import pickle from test.support import requires_working_socket, check_syntax_error, run_code from typing import Generic, Sequence, TypeVar, TypeVarTuple, ParamSpec, get_args @@ -855,3 +856,68 @@ def func[A](): ns = run_code(code) self.assertEqual(ns["func"].__type_params__, ()) + + + +# All these type aliases are used for pickling tests: +T = TypeVar('T') +def func1[X](x: X) -> X: ... +def func2[X, Y](x: X | Y) -> X | Y: ... +def func3[X, *Y, **Z](x: X, y: tuple[*Y], z: Z) -> X: ... +def func4[X: int, Y: (bytes, str)](x: X, y: Y) -> X | Y: ... + +class Class1[X]: ... +class Class2[X, Y]: ... +class Class3[X, *Y, **Z]: ... +class Class4[X: int, Y: (bytes, str)]: ... + + +class TypeParamsPickleTest(unittest.TestCase): + def test_pickling_functions(self): + things_to_test = [ + func1, + func2, + func3, + func4, + ] + for thing in things_to_test: + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(thing=thing, proto=proto): + pickled = pickle.dumps(thing, protocol=proto) + self.assertEqual(pickle.loads(pickled), thing) + + def test_pickling_classes(self): + things_to_test = [ + Class1, + Class1[int], + Class1[T], + + Class2, + Class2[int, T], + Class2[T, int], + Class2[int, str], + + Class3, + Class3[int, T, str, bytes, [float, object, T]], + + Class4, + Class4[int, bytes], + Class4[T, bytes], + Class4[int, T], + Class4[T, T], + ] + for thing in things_to_test: + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(thing=thing, proto=proto): + pickled = pickle.dumps(thing, protocol=proto) + self.assertEqual(pickle.loads(pickled), thing) + + for klass in things_to_test: + real_class = getattr(klass, '__origin__', klass) + thing = klass() + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + with self.subTest(thing=thing, proto=proto): + pickled = pickle.dumps(thing, protocol=proto) + # These instances are not equal, + # but class check is good enough: + self.assertIsInstance(pickle.loads(pickled), real_class) From 0d0963737a0f4b7cadedfae7e8fd33ed18269289 Mon Sep 17 00:00:00 2001 From: chgnrdv <52372310+chgnrdv@users.noreply.github.com> Date: Fri, 16 Jun 2023 10:10:59 +0300 Subject: [PATCH 5/9] Fix inaccuracies in "Assorted Topics" section of "Defining Extension Types" tutorial (#104969) --- Doc/extending/newtypes.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/extending/newtypes.rst b/Doc/extending/newtypes.rst index 6852a385f0c63c..7f8f8ddaaaccd6 100644 --- a/Doc/extending/newtypes.rst +++ b/Doc/extending/newtypes.rst @@ -168,7 +168,7 @@ representation of the instance for which it is called. Here is a simple example:: static PyObject * - newdatatype_repr(newdatatypeobject * obj) + newdatatype_repr(newdatatypeobject *obj) { return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}", obj->obj_UnderlyingDatatypePtr->size); @@ -188,7 +188,7 @@ used instead. Here is a simple example:: static PyObject * - newdatatype_str(newdatatypeobject * obj) + newdatatype_str(newdatatypeobject *obj) { return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}", obj->obj_UnderlyingDatatypePtr->size); @@ -338,7 +338,7 @@ Here is an example:: PyErr_Format(PyExc_AttributeError, "'%.100s' object has no attribute '%.400s'", - tp->tp_name, name); + Py_TYPE(obj)->tp_name, name); return NULL; } @@ -379,7 +379,7 @@ Here is a sample implementation, for a datatype that is considered equal if the size of an internal pointer is equal:: static PyObject * - newdatatype_richcmp(PyObject *obj1, PyObject *obj2, int op) + newdatatype_richcmp(newdatatypeobject *obj1, newdatatypeobject *obj2, int op) { PyObject *result; int c, size1, size2; @@ -478,7 +478,7 @@ This function takes three arguments: Here is a toy ``tp_call`` implementation:: static PyObject * - newdatatype_call(newdatatypeobject *self, PyObject *args, PyObject *kwds) + newdatatype_call(newdatatypeobject *obj, PyObject *args, PyObject *kwds) { PyObject *result; const char *arg1; From 0bffe1acd78069ea21f6b1347bec9cc9747342cb Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Fri, 16 Jun 2023 10:41:47 +0200 Subject: [PATCH 6/9] gh-105844: Consistently use 'minor version' for X.Y versions (#105851) --- Doc/faq/general.rst | 4 ++-- Doc/install/index.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/faq/general.rst b/Doc/faq/general.rst index a9b2622e02ef3b..298ce111698a65 100644 --- a/Doc/faq/general.rst +++ b/Doc/faq/general.rst @@ -135,7 +135,7 @@ Python versions are numbered "A.B.C" or "A.B": See :pep:`6` for more information about bugfix releases. -Not all releases are bugfix releases. In the run-up to a new major release, a +Not all releases are bugfix releases. In the run-up to a new minor release, a series of development releases are made, denoted as alpha, beta, or release candidate. Alphas are early releases in which interfaces aren't yet finalized; it's not unexpected to see an interface change between two alpha releases. @@ -297,7 +297,7 @@ How stable is Python? Very stable. New, stable releases have been coming out roughly every 6 to 18 months since 1991, and this seems likely to continue. As of version 3.9, -Python will have a major new release every 12 months (:pep:`602`). +Python will have a minor new release every 12 months (:pep:`602`). The developers issue "bugfix" releases of older versions, so the stability of existing releases gradually improves. Bugfix releases, indicated by a third diff --git a/Doc/install/index.rst b/Doc/install/index.rst index ab581d785ef7f0..beb34f0cf21b22 100644 --- a/Doc/install/index.rst +++ b/Doc/install/index.rst @@ -696,7 +696,7 @@ is supplied to suppress this behaviour. So you could simply edit import sys sys.path.append('/www/python/') -However, if you reinstall the same major version of Python (perhaps when +However, if you reinstall the same minor version of Python (perhaps when upgrading from 2.2 to 2.2.2, for example) :file:`site.py` will be overwritten by the stock version. You'd have to remember that it was modified and save a copy before doing the installation. From 101d5ec7d7fe122fa81a377c8ab8b562d1add9ee Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 16 Jun 2023 14:04:34 +0300 Subject: [PATCH 7/9] CI: Remove docs build from Azure Pipelines (#105823) --- .azure-pipelines/ci.yml | 14 --------- .azure-pipelines/docs-steps.yml | 47 ---------------------------- .azure-pipelines/pr.yml | 12 ------- .azure-pipelines/prebuild-checks.yml | 12 ------- 4 files changed, 85 deletions(-) delete mode 100644 .azure-pipelines/docs-steps.yml diff --git a/.azure-pipelines/ci.yml b/.azure-pipelines/ci.yml index fb4a2218ddd8d8..63252a76abb69f 100644 --- a/.azure-pipelines/ci.yml +++ b/.azure-pipelines/ci.yml @@ -14,20 +14,6 @@ jobs: - template: ./prebuild-checks.yml -- job: Docs_PR - displayName: Docs PR - dependsOn: Prebuild - condition: and(succeeded(), eq(dependencies.Prebuild.outputs['docs.run'], 'true')) - - pool: - vmImage: ubuntu-22.04 - - steps: - - template: ./docs-steps.yml - parameters: - upload: true - - - job: macOS_CI_Tests displayName: macOS CI Tests dependsOn: Prebuild diff --git a/.azure-pipelines/docs-steps.yml b/.azure-pipelines/docs-steps.yml deleted file mode 100644 index 647daff7a033a8..00000000000000 --- a/.azure-pipelines/docs-steps.yml +++ /dev/null @@ -1,47 +0,0 @@ -parameters: - latex: false - upload: false - -steps: -- checkout: self - clean: true - fetchDepth: 5 - -- task: UsePythonVersion@0 - displayName: 'Use Python 3.6 or later' - inputs: - versionSpec: '>=3.6' - -- script: python -m pip install -r requirements.txt - workingDirectory: '$(build.sourcesDirectory)/Doc' - displayName: 'Install build dependencies' - -- ${{ if ne(parameters.latex, 'true') }}: - - script: make check html PYTHON=python - workingDirectory: '$(build.sourcesDirectory)/Doc' - displayName: 'Build documentation' - -- ${{ if eq(parameters.latex, 'true') }}: - - script: sudo apt-get update && sudo apt-get install -qy --force-yes texlive-full - displayName: 'Install LaTeX' - - - script: make dist PYTHON=python SPHINXBUILD='python -m sphinx' BLURB='python -m blurb' - workingDirectory: '$(build.sourcesDirectory)/Doc' - displayName: 'Build documentation' - -- ${{ if eq(parameters.upload, 'true') }}: - - task: PublishBuildArtifacts@1 - displayName: 'Publish docs' - - inputs: - PathToPublish: '$(build.sourcesDirectory)/Doc/build' - ArtifactName: docs - publishLocation: Container - - - ${{ if eq(parameters.latex, 'true') }}: - - task: PublishBuildArtifacts@1 - displayName: 'Publish dist' - inputs: - PathToPublish: '$(build.sourcesDirectory)/Doc/dist' - ArtifactName: docs_dist - publishLocation: Container diff --git a/.azure-pipelines/pr.yml b/.azure-pipelines/pr.yml index b822d58806b9a6..939c9b4249a3c2 100644 --- a/.azure-pipelines/pr.yml +++ b/.azure-pipelines/pr.yml @@ -14,18 +14,6 @@ jobs: - template: ./prebuild-checks.yml -- job: Docs_PR - displayName: Docs PR - dependsOn: Prebuild - condition: and(succeeded(), eq(dependencies.Prebuild.outputs['docs.run'], 'true')) - - pool: - vmImage: ubuntu-22.04 - - steps: - - template: ./docs-steps.yml - - - job: macOS_PR_Tests displayName: macOS PR Tests dependsOn: Prebuild diff --git a/.azure-pipelines/prebuild-checks.yml b/.azure-pipelines/prebuild-checks.yml index 30ff642d1267a1..2c6460d2386735 100644 --- a/.azure-pipelines/prebuild-checks.yml +++ b/.azure-pipelines/prebuild-checks.yml @@ -11,18 +11,6 @@ steps: displayName: Fetch comparison tree condition: and(succeeded(), variables['System.PullRequest.TargetBranch']) -- script: | - if ! git diff --name-only $(diffTarget) | grep -qE '(\.rst$|^Doc|^Misc)' - then - echo "No docs were updated: docs.run=false" - echo "##vso[task.setvariable variable=run;isOutput=true]false" - else - echo "Docs were updated: docs.run=true" - echo "##vso[task.setvariable variable=run;isOutput=true]true" - fi - displayName: Detect documentation changes - name: docs - - script: | if ! git diff --name-only $(diffTarget) | grep -qvE '(\.rst$|^Doc|^Misc)' then From 70c075c194d3739ae10ce76265f05fa82ed46487 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 16 Jun 2023 16:47:55 +0100 Subject: [PATCH 8/9] gh-105834: Add tests for calling `issubclass()` between two protocols (#105835) Some parts of the implementation of `typing.Protocol` had poor test coverage --- Lib/test/test_typing.py | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 3eb0fcad69e5e5..ad67568770970f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2759,6 +2759,80 @@ def x(self): ... with self.assertRaisesRegex(TypeError, only_classes_allowed): issubclass(1, BadPG) + def test_implicit_issubclass_between_two_protocols(self): + @runtime_checkable + class CallableMembersProto(Protocol): + def meth(self): ... + + # All the below protocols should be considered "subclasses" + # of CallableMembersProto at runtime, + # even though none of them explicitly subclass CallableMembersProto + + class IdenticalProto(Protocol): + def meth(self): ... + + class SupersetProto(Protocol): + def meth(self): ... + def meth2(self): ... + + class NonCallableMembersProto(Protocol): + meth: Callable[[], None] + + class NonCallableMembersSupersetProto(Protocol): + meth: Callable[[], None] + meth2: Callable[[str, int], bool] + + class MixedMembersProto1(Protocol): + meth: Callable[[], None] + def meth2(self): ... + + class MixedMembersProto2(Protocol): + def meth(self): ... + meth2: Callable[[str, int], bool] + + for proto in ( + IdenticalProto, SupersetProto, NonCallableMembersProto, + NonCallableMembersSupersetProto, MixedMembersProto1, MixedMembersProto2 + ): + with self.subTest(proto=proto.__name__): + self.assertIsSubclass(proto, CallableMembersProto) + + # These two shouldn't be considered subclasses of CallableMembersProto, however, + # since they don't have the `meth` protocol member + + class EmptyProtocol(Protocol): ... + class UnrelatedProtocol(Protocol): + def wut(self): ... + + self.assertNotIsSubclass(EmptyProtocol, CallableMembersProto) + self.assertNotIsSubclass(UnrelatedProtocol, CallableMembersProto) + + # These aren't protocols at all (despite having annotations), + # so they should only be considered subclasses of CallableMembersProto + # if they *actually have an attribute* matching the `meth` member + # (just having an annotation is insufficient) + + class AnnotatedButNotAProtocol: + meth: Callable[[], None] + + class NotAProtocolButAnImplicitSubclass: + def meth(self): pass + + class NotAProtocolButAnImplicitSubclass2: + meth: Callable[[], None] + def meth(self): pass + + class NotAProtocolButAnImplicitSubclass3: + meth: Callable[[], None] + meth2: Callable[[int, str], bool] + def meth(self): pass + def meth(self, x, y): return True + + self.assertNotIsSubclass(AnnotatedButNotAProtocol, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass2, CallableMembersProto) + self.assertIsSubclass(NotAProtocolButAnImplicitSubclass3, CallableMembersProto) + def test_isinstance_checks_not_at_whim_of_gc(self): self.addCleanup(gc.enable) gc.disable() From 957a974d4fc1575787e4a29a399a47520d6df6d3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 16 Jun 2023 09:31:23 -0700 Subject: [PATCH 9/9] gh-104799: PEP 695 backward compatibility for ast.unparse (#105846) --- Lib/ast.py | 6 +- Lib/test/test_unparse.py | 74 ++++++++++++++++++- ...-06-15-18-11-47.gh-issue-104799.BcLzbP.rst | 3 + 3 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-06-15-18-11-47.gh-issue-104799.BcLzbP.rst diff --git a/Lib/ast.py b/Lib/ast.py index 226910ecac0b4a..a307f3ecd06175 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -1051,7 +1051,8 @@ def visit_ClassDef(self, node): self.fill("@") self.traverse(deco) self.fill("class " + node.name) - self._type_params_helper(node.type_params) + if hasattr(node, "type_params"): + self._type_params_helper(node.type_params) with self.delimit_if("(", ")", condition = node.bases or node.keywords): comma = False for e in node.bases: @@ -1083,7 +1084,8 @@ def _function_helper(self, node, fill_suffix): self.traverse(deco) def_str = fill_suffix + " " + node.name self.fill(def_str) - self._type_params_helper(node.type_params) + if hasattr(node, "type_params"): + self._type_params_helper(node.type_params) with self.delimit("(", ")"): self.traverse(node.args) if node.returns: diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py index 88c7c3a0af8771..41a6318d1499b4 100644 --- a/Lib/test/test_unparse.py +++ b/Lib/test/test_unparse.py @@ -1,4 +1,4 @@ -"""Tests for the unparse.py script in the Tools/parser directory.""" +"""Tests for ast.unparse.""" import unittest import test.support @@ -625,6 +625,78 @@ def test_star_expr_assign_target_multiple(self): self.check_src_roundtrip("a, b = [c, d] = e, f = g") +class ManualASTCreationTestCase(unittest.TestCase): + """Test that AST nodes created without a type_params field unparse correctly.""" + + def test_class(self): + node = ast.ClassDef(name="X", bases=[], keywords=[], body=[ast.Pass()], decorator_list=[]) + ast.fix_missing_locations(node) + self.assertEqual(ast.unparse(node), "class X:\n pass") + + def test_class_with_type_params(self): + node = ast.ClassDef(name="X", bases=[], keywords=[], body=[ast.Pass()], decorator_list=[], + type_params=[ast.TypeVar("T")]) + ast.fix_missing_locations(node) + self.assertEqual(ast.unparse(node), "class X[T]:\n pass") + + def test_function(self): + node = ast.FunctionDef( + name="f", + args=ast.arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), + body=[ast.Pass()], + decorator_list=[], + returns=None, + ) + ast.fix_missing_locations(node) + self.assertEqual(ast.unparse(node), "def f():\n pass") + + def test_function_with_type_params(self): + node = ast.FunctionDef( + name="f", + args=ast.arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), + body=[ast.Pass()], + decorator_list=[], + returns=None, + type_params=[ast.TypeVar("T")], + ) + ast.fix_missing_locations(node) + self.assertEqual(ast.unparse(node), "def f[T]():\n pass") + + def test_function_with_type_params_and_bound(self): + node = ast.FunctionDef( + name="f", + args=ast.arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), + body=[ast.Pass()], + decorator_list=[], + returns=None, + type_params=[ast.TypeVar("T", bound=ast.Name("int"))], + ) + ast.fix_missing_locations(node) + self.assertEqual(ast.unparse(node), "def f[T: int]():\n pass") + + def test_async_function(self): + node = ast.AsyncFunctionDef( + name="f", + args=ast.arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), + body=[ast.Pass()], + decorator_list=[], + returns=None, + ) + ast.fix_missing_locations(node) + self.assertEqual(ast.unparse(node), "async def f():\n pass") + + def test_async_function_with_type_params(self): + node = ast.AsyncFunctionDef( + name="f", + args=ast.arguments(posonlyargs=[], args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), + body=[ast.Pass()], + decorator_list=[], + returns=None, + type_params=[ast.TypeVar("T")], + ) + ast.fix_missing_locations(node) + self.assertEqual(ast.unparse(node), "async def f[T]():\n pass") + class DirectoryTestCase(ASTTestCase): """Test roundtrip behaviour on all files in Lib and Lib/test.""" diff --git a/Misc/NEWS.d/next/Library/2023-06-15-18-11-47.gh-issue-104799.BcLzbP.rst b/Misc/NEWS.d/next/Library/2023-06-15-18-11-47.gh-issue-104799.BcLzbP.rst new file mode 100644 index 00000000000000..d0dbff4f1553e2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-06-15-18-11-47.gh-issue-104799.BcLzbP.rst @@ -0,0 +1,3 @@ +Enable :func:`ast.unparse` to unparse function and class definitions created +without the new ``type_params`` field from :pep:`695`. Patch by Jelle +Zijlstra.