From 68e435cc6666ce17bbd71653616c29c1d88d7ef3 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Jun 2024 12:43:40 +0100 Subject: [PATCH 1/8] Update 'next' signature in Python 3.13 where it's been fixed --- test/test_inference/test_signature.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_inference/test_signature.py b/test/test_inference/test_signature.py index f8f71581a..4a1fcb62e 100644 --- a/test/test_inference/test_signature.py +++ b/test/test_inference/test_signature.py @@ -1,5 +1,5 @@ from textwrap import dedent -from operator import ge, lt +from operator import eq, ge, lt import re import os @@ -14,7 +14,8 @@ ('import math; math.cos', 'cos(x, /)', ['x'], ge, (3, 6)), ('next', 'next(iterator, default=None, /)', ['iterator', 'default'], lt, (3, 12)), - ('next', 'next()', [], ge, (3, 12)), + ('next', 'next()', [], eq, (3, 12)), + ('next', 'next(iterator, default=None, /)', ['iterator', 'default'], ge, (3, 13)), ('str', "str(object='', /) -> str", ['object'], ge, (3, 6)), From ee90cd97b614f02460e82a250ff8e26a35d23f8d Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Jun 2024 12:51:23 +0100 Subject: [PATCH 2/8] Name this list of accepted symbol differences This should make it easier to add new entries as well as clarifying the intent of this filter. --- test/test_utils.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/test_utils.py b/test/test_utils.py index acc1df9c2..31637d47c 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -73,15 +73,19 @@ def test_import(self): import os s = 'from os import ' goal = {s + el for el in dir(os)} + # There are minor differences, e.g. the dir doesn't include deleted # items as well as items that are not only available on linux. difference = set(self.complete(s)).symmetric_difference(goal) + ACCEPTED_DIFFERENCE_PREFIXES = [ + '_', 'O_', 'EX_', 'MFD_', + 'SF_', 'ST_', 'CLD_', 'POSIX_SPAWN_', 'P_', + 'RWF_', 'CLONE_', 'SCHED_', + ] difference = { x for x in difference - if all(not x.startswith('from os import ' + s) - for s in ['_', 'O_', 'EX_', 'MFD_', 'SF_', 'ST_', - 'CLD_', 'POSIX_SPAWN_', 'P_', 'RWF_', - 'CLONE_', 'SCHED_']) + if all(not x.startswith('from os import ' + prefix) + for prefix in ACCEPTED_DIFFERENCE_PREFIXES) } # There are quite a few differences, because both Windows and Linux # (posix and nt) libraries are included. From bbbaad21e828739a20c1bc0aead5e8de94863354 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Jun 2024 12:55:12 +0100 Subject: [PATCH 3/8] Clarify filter by flipping the boolean logic --- test/test_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_utils.py b/test/test_utils.py index 31637d47c..9a1f91108 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -84,8 +84,10 @@ def test_import(self): ] difference = { x for x in difference - if all(not x.startswith('from os import ' + prefix) - for prefix in ACCEPTED_DIFFERENCE_PREFIXES) + if not any( + x.startswith('from os import ' + prefix) + for prefix in ACCEPTED_DIFFERENCE_PREFIXES + ) } # There are quite a few differences, because both Windows and Linux # (posix and nt) libraries are included. From 473b35e6ec1f51493b372d16449dbc699661d69c Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Jun 2024 12:56:48 +0100 Subject: [PATCH 4/8] Ignore more items extra in Python 3.13 --- test/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_utils.py b/test/test_utils.py index 9a1f91108..15a791744 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -78,9 +78,9 @@ def test_import(self): # items as well as items that are not only available on linux. difference = set(self.complete(s)).symmetric_difference(goal) ACCEPTED_DIFFERENCE_PREFIXES = [ - '_', 'O_', 'EX_', 'MFD_', + '_', 'O_', 'EX_', 'EFD_', 'MFD_', 'TFD_', 'SF_', 'ST_', 'CLD_', 'POSIX_SPAWN_', 'P_', - 'RWF_', 'CLONE_', 'SCHED_', + 'RWF_', 'CLONE_', 'SCHED_', 'SPLICE_', ] difference = { x for x in difference From 340dedd021db59362639965af9cbe7d945846a10 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Jun 2024 13:01:58 +0100 Subject: [PATCH 5/8] Use an explicit mapping for locals in this test In Python 3.13 the `locals` function now returns a fresh mapping each time it's called (when called in a function). We thus need to store a reference to the mapping being used, rather than re-fetching it each time. Since we don't actually need to modify the locals within the scope of the test function itself, it suffices to use our own mapping here rather than the result of calling `locals`, which fully isolates this test from the nature of that function. Fixes https://github.com/davidhalter/jedi/issues/2002 --- test/test_api/test_interpreter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index c435db9c4..c0099c409 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -310,8 +310,9 @@ def test_completion_param_annotations(): # Need to define this function not directly in Python. Otherwise Jedi is too # clever and uses the Python code instead of the signature object. code = 'def foo(a: 1, b: str, c: int = 1.0) -> bytes: pass' - exec(code, locals()) - script = jedi.Interpreter('foo', [locals()]) + exec_locals = {} + exec(code, exec_locals) + script = jedi.Interpreter('foo', [exec_locals]) c, = script.complete() sig, = c.get_signatures() a, b, c = sig.params @@ -323,7 +324,7 @@ def test_completion_param_annotations(): assert b.description == 'param b: str' assert c.description == 'param c: int=1.0' - d, = jedi.Interpreter('foo()', [locals()]).infer() + d, = jedi.Interpreter('foo()', [exec_locals]).infer() assert d.name == 'bytes' From d543d1d004978d625dbe8edeff5756ef651cbc61 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 23 Jun 2024 13:13:31 +0100 Subject: [PATCH 6/8] Support Python 3.13 This moves to using the 3.13 grammar as well as testing 3.13 in CI. --- .github/workflows/ci.yml | 4 ++-- CHANGELOG.rst | 2 ++ jedi/api/environment.py | 2 +- jedi/inference/__init__.py | 2 +- setup.py | 5 +++-- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c236f4e2b..d32628338 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,8 @@ jobs: strategy: matrix: os: [ubuntu-20.04, windows-2019] - python-version: ["3.12", "3.11", "3.10", "3.9", "3.8", "3.7", "3.6"] - environment: ['3.8', '3.12', '3.11', '3.10', '3.9', '3.7', '3.6', 'interpreter'] + python-version: ["3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "3.7", "3.6"] + environment: ['3.8', '3.13', '3.12', '3.11', '3.10', '3.9', '3.7', '3.6', 'interpreter'] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fca944293..cf6810fa0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Changelog Unreleased ++++++++++ +- Python 3.13 support + 0.19.1 (2023-10-02) +++++++++++++++++++ diff --git a/jedi/api/environment.py b/jedi/api/environment.py index 278899aaa..64b318e1f 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -22,7 +22,7 @@ _VersionInfo = namedtuple('VersionInfo', 'major minor micro') # type: ignore[name-match] -_SUPPORTED_PYTHONS = ['3.12', '3.11', '3.10', '3.9', '3.8', '3.7', '3.6'] +_SUPPORTED_PYTHONS = ['3.13', '3.12', '3.11', '3.10', '3.9', '3.8', '3.7', '3.6'] _SAFE_PATHS = ['/usr/bin', '/usr/local/bin'] _CONDA_VAR = 'CONDA_PREFIX' _CURRENT_VERSION = '%s.%s' % (sys.version_info.major, sys.version_info.minor) diff --git a/jedi/inference/__init__.py b/jedi/inference/__init__.py index aadfeba96..bd31cbd39 100644 --- a/jedi/inference/__init__.py +++ b/jedi/inference/__init__.py @@ -90,7 +90,7 @@ def __init__(self, project, environment=None, script_path=None): self.compiled_subprocess = environment.get_inference_state_subprocess(self) self.grammar = environment.get_grammar() - self.latest_grammar = parso.load_grammar(version='3.12') + self.latest_grammar = parso.load_grammar(version='3.13') self.memoize_cache = {} # for memoize decorators self.module_cache = imports.ModuleCache() # does the job of `sys.modules`. self.stub_module_cache = {} # Dict[Tuple[str, ...], Optional[ModuleValue]] diff --git a/setup.py b/setup.py index 43292a4be..54b5b0a12 100755 --- a/setup.py +++ b/setup.py @@ -35,8 +35,8 @@ long_description=readme, packages=find_packages(exclude=['test', 'test.*']), python_requires='>=3.6', - # Python 3.11 & 3.12 grammars are added to parso in 0.8.3 - install_requires=['parso>=0.8.3,<0.9.0'], + # Python 3.13 grammars are added to parso in 0.8.4 + install_requires=['parso>=0.8.4,<0.9.0'], extras_require={ 'testing': [ 'pytest<9.0.0', @@ -101,6 +101,7 @@ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Text Editors :: Integrated Development Environments (IDE)', 'Topic :: Utilities', From 255186376e6fd1d3460a0d0541a3dceb436a52b1 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sat, 29 Jun 2024 16:38:27 +0100 Subject: [PATCH 7/8] Cope with Python 3.13 moving pathlib's implementation Jedi passes pickles to subprocesses which are running the target version of Python and thus may not be the same as the version under which Jedi itself is running. In Python 3.13, pathlib is being refactored to allow for easier extension and has thus moved most of its internal implementation to a submodule. Unfortunately this changes the paths of the symbols, causing pickles of those types to fail to load in earlier versions of Python. This commit introduces a custom unpickler which accounts for this move, allowing bi-directional passing of pickles to work. --- jedi/_compatibility.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 13a74b7b3..48563d00d 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -5,11 +5,24 @@ import errno import sys import pickle +from typing import Any + + +class Unpickler(pickle.Unpickler): + def find_class(self, module: str, name: str) -> Any: + # Python 3.13 moved pathlib implementation out of __init__.py as part of + # generalising its implementation. Ensure that we support loading + # pickles from 3.13 on older version of Python. Since 3.13 maintained a + # compatible API, pickles from older Python work natively on the newer + # version. + if module == 'pathlib._local': + module = 'pathlib' + return super().find_class(module, name) def pickle_load(file): try: - return pickle.load(file) + return Unpickler(file).load() # Python on Windows don't throw EOF errors for pipes. So reraise them with # the correct type, which is caught upwards. except OSError: From 857c9be500eda6b94d33732387812e599b5cf078 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Fri, 5 Jul 2024 21:36:23 +0100 Subject: [PATCH 8/8] Ignore py__name__ issues for functools.partial in Python 3.13.0b3+ See https://github.com/davidhalter/jedi/issues/2012 for details. --- test/test_api/test_interpreter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_api/test_interpreter.py b/test/test_api/test_interpreter.py index c0099c409..1aa027bf4 100644 --- a/test/test_api/test_interpreter.py +++ b/test/test_api/test_interpreter.py @@ -526,10 +526,14 @@ def func(a, b, c): c = functools.partial(func, 1, c=2) sig, = jedi.Interpreter(code, [locals()]).get_signatures() - assert sig.name == 'partial' assert [p.name for p in sig.params] == expected assert index == sig.index + if sys.version_info < (3, 13): + # Python 3.13.0b3 makes functools.partial be a descriptor, which breaks + # Jedi's `py__name__` detection; see https://github.com/davidhalter/jedi/issues/2012 + assert sig.name == 'partial' + def test_type_var(): """This was an issue before, see Github #1369"""