From 6444a924bf04445714c1b8f0c5828692fc94f801 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel@debian.org>
Date: Sun, 9 Feb 2025 18:29:58 +0100
Subject: [PATCH] Adding upstream version 3.0.23.

Signed-off-by: Daniel Baumann <daniel@debian.org>
---
 .github/workflows/test.yaml            |  2 +-
 CHANGELOG                              | 11 +++
 README.rst                             |  2 +-
 docs/concurrency-challenges.rst        |  2 +-
 examples/ptpython_config/config.py     |  3 +-
 ptpython/__init__.py                   |  2 +
 ptpython/__main__.py                   |  2 +
 ptpython/completer.py                  | 51 +++++++-------
 ptpython/contrib/asyncssh_repl.py      |  4 +-
 ptpython/entry_points/run_ptipython.py |  4 +-
 ptpython/entry_points/run_ptpython.py  |  6 +-
 ptpython/eventloop.py                  |  2 +
 ptpython/filters.py                    |  4 +-
 ptpython/history_browser.py            | 34 ++++-----
 ptpython/ipython.py                    |  6 +-
 ptpython/key_bindings.py               | 10 +--
 ptpython/layout.py                     | 48 +++++++------
 ptpython/lexer.py                      |  4 +-
 ptpython/prompt_style.py               |  4 +-
 ptpython/python_input.py               | 98 +++++++++++++++++---------
 ptpython/repl.py                       | 27 +++----
 ptpython/signatures.py                 | 24 +++----
 ptpython/style.py                      |  8 ++-
 ptpython/utils.py                      |  8 ++-
 ptpython/validator.py                  |  4 +-
 setup.py                               | 10 +--
 tests/run_tests.py                     |  2 +
 27 files changed, 230 insertions(+), 152 deletions(-)

diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 7ec8662..31837db 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.7, 3.8, 3.9, "3.10"]
+        python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
 
     steps:
       - uses: actions/checkout@v2
diff --git a/CHANGELOG b/CHANGELOG
index 916a542..645ca60 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,17 @@
 CHANGELOG
 =========
 
+3.0.23: 2023-02-22
+------------------
+
+Fixes:
+- Don't print exception messages twice for unhandled exceptions.
+- Added cursor shape support.
+
+Breaking changes:
+- Drop Python 3.6 support.
+
+
 3.0.22: 2022-12-06
 ------------------
 
diff --git a/README.rst b/README.rst
index 15464ba..2db3f69 100644
--- a/README.rst
+++ b/README.rst
@@ -213,7 +213,7 @@ This is also available for embedding:
 
 .. code:: python
 
-    from ptpython.ipython.repl import embed
+    from ptpython.ipython import embed
     embed(globals(), locals())
 
 
diff --git a/docs/concurrency-challenges.rst b/docs/concurrency-challenges.rst
index b56d969..0ff9c6c 100644
--- a/docs/concurrency-challenges.rst
+++ b/docs/concurrency-challenges.rst
@@ -67,7 +67,7 @@ When a normal blocking embed is used:
 When an awaitable embed is used, for embedding in a coroutine, but having the
 event loop continue:
     * We run the input method from the blocking embed in an asyncio executor
-      and do an `await loop.run_in_excecutor(...)`.
+      and do an `await loop.run_in_executor(...)`.
     * The "eval" happens again in the main thread.
     * "print" is also similar, except that the pager code (if used) runs in an
       executor too.
diff --git a/examples/ptpython_config/config.py b/examples/ptpython_config/config.py
index bf9d05f..2f3f49d 100644
--- a/examples/ptpython_config/config.py
+++ b/examples/ptpython_config/config.py
@@ -3,6 +3,7 @@ Configuration example for ``ptpython``.
 
 Copy this file to $XDG_CONFIG_HOME/ptpython/config.py
 On Linux, this is: ~/.config/ptpython/config.py
+On macOS, this is: ~/Library/Application Support/ptpython/config.py
 """
 from prompt_toolkit.filters import ViInsertMode
 from prompt_toolkit.key_binding.key_processor import KeyPress
@@ -49,7 +50,7 @@ def configure(repl):
     # Swap light/dark colors on or off
     repl.swap_light_and_dark = False
 
-    # Highlight matching parethesis.
+    # Highlight matching parentheses.
     repl.highlight_matching_parenthesis = True
 
     # Line wrapping. (Instead of horizontal scrolling.)
diff --git a/ptpython/__init__.py b/ptpython/__init__.py
index 4908eba..63c6233 100644
--- a/ptpython/__init__.py
+++ b/ptpython/__init__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from .repl import embed
 
 __all__ = ["embed"]
diff --git a/ptpython/__main__.py b/ptpython/__main__.py
index 83340a7..c006261 100644
--- a/ptpython/__main__.py
+++ b/ptpython/__main__.py
@@ -1,6 +1,8 @@
 """
 Make `python -m ptpython` an alias for running `./ptpython`.
 """
+from __future__ import annotations
+
 from .entry_points.run_ptpython import run
 
 run()
diff --git a/ptpython/completer.py b/ptpython/completer.py
index 9252106..f28d2b1 100644
--- a/ptpython/completer.py
+++ b/ptpython/completer.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import ast
 import collections.abc as collections_abc
 import inspect
@@ -44,8 +46,8 @@ class PythonCompleter(Completer):
 
     def __init__(
         self,
-        get_globals: Callable[[], Dict[str, Any]],
-        get_locals: Callable[[], Dict[str, Any]],
+        get_globals: Callable[[], dict[str, Any]],
+        get_locals: Callable[[], dict[str, Any]],
         enable_dictionary_completion: Callable[[], bool],
     ) -> None:
         super().__init__()
@@ -58,8 +60,8 @@ class PythonCompleter(Completer):
         self._jedi_completer = JediCompleter(get_globals, get_locals)
         self._dictionary_completer = DictionaryCompleter(get_globals, get_locals)
 
-        self._path_completer_cache: Optional[GrammarCompleter] = None
-        self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None
+        self._path_completer_cache: GrammarCompleter | None = None
+        self._path_completer_grammar_cache: _CompiledGrammar | None = None
 
     @property
     def _path_completer(self) -> GrammarCompleter:
@@ -74,7 +76,7 @@ class PythonCompleter(Completer):
         return self._path_completer_cache
 
     @property
-    def _path_completer_grammar(self) -> "_CompiledGrammar":
+    def _path_completer_grammar(self) -> _CompiledGrammar:
         """
         Return the grammar for matching paths inside strings inside Python
         code.
@@ -85,7 +87,7 @@ class PythonCompleter(Completer):
             self._path_completer_grammar_cache = self._create_path_completer_grammar()
         return self._path_completer_grammar_cache
 
-    def _create_path_completer_grammar(self) -> "_CompiledGrammar":
+    def _create_path_completer_grammar(self) -> _CompiledGrammar:
         def unwrapper(text: str) -> str:
             return re.sub(r"\\(.)", r"\1", text)
 
@@ -189,7 +191,6 @@ class PythonCompleter(Completer):
         ):
             # If we are inside a string, Don't do Jedi completion.
             if not self._path_completer_grammar.match(document.text_before_cursor):
-
                 # Do Jedi Python completions.
                 yield from self._jedi_completer.get_completions(
                     document, complete_event
@@ -203,8 +204,8 @@ class JediCompleter(Completer):
 
     def __init__(
         self,
-        get_globals: Callable[[], Dict[str, Any]],
-        get_locals: Callable[[], Dict[str, Any]],
+        get_globals: Callable[[], dict[str, Any]],
+        get_locals: Callable[[], dict[str, Any]],
     ) -> None:
         super().__init__()
 
@@ -242,7 +243,7 @@ class JediCompleter(Completer):
                 # Jedi issue: "KeyError: u'a_lambda'."
                 # https://github.com/jonathanslenders/ptpython/issues/89
                 pass
-            except IOError:
+            except OSError:
                 # Jedi issue: "IOError: No such file or directory."
                 # https://github.com/jonathanslenders/ptpython/issues/71
                 pass
@@ -303,8 +304,8 @@ class DictionaryCompleter(Completer):
 
     def __init__(
         self,
-        get_globals: Callable[[], Dict[str, Any]],
-        get_locals: Callable[[], Dict[str, Any]],
+        get_globals: Callable[[], dict[str, Any]],
+        get_locals: Callable[[], dict[str, Any]],
     ) -> None:
         super().__init__()
 
@@ -386,7 +387,7 @@ class DictionaryCompleter(Completer):
             re.VERBOSE,
         )
 
-    def _lookup(self, expression: str, temp_locals: Dict[str, Any]) -> object:
+    def _lookup(self, expression: str, temp_locals: dict[str, Any]) -> object:
         """
         Do lookup of `object_var` in the context.
         `temp_locals` is a dictionary, used for the locals.
@@ -399,7 +400,6 @@ class DictionaryCompleter(Completer):
     def get_completions(
         self, document: Document, complete_event: CompleteEvent
     ) -> Iterable[Completion]:
-
         # First, find all for-loops, and assign the first item of the
         # collections they're iterating to the iterator variable, so that we
         # can provide code completion on the iterators.
@@ -431,7 +431,7 @@ class DictionaryCompleter(Completer):
         except BaseException:
             raise ReprFailedError
 
-    def eval_expression(self, document: Document, locals: Dict[str, Any]) -> object:
+    def eval_expression(self, document: Document, locals: dict[str, Any]) -> object:
         """
         Evaluate
         """
@@ -446,7 +446,7 @@ class DictionaryCompleter(Completer):
         self,
         document: Document,
         complete_event: CompleteEvent,
-        temp_locals: Dict[str, Any],
+        temp_locals: dict[str, Any],
     ) -> Iterable[Completion]:
         """
         Complete the [ or . operator after an object.
@@ -454,7 +454,6 @@ class DictionaryCompleter(Completer):
         result = self.eval_expression(document, temp_locals)
 
         if result is not None:
-
             if isinstance(
                 result,
                 (list, tuple, dict, collections_abc.Mapping, collections_abc.Sequence),
@@ -470,7 +469,7 @@ class DictionaryCompleter(Completer):
         self,
         document: Document,
         complete_event: CompleteEvent,
-        temp_locals: Dict[str, Any],
+        temp_locals: dict[str, Any],
     ) -> Iterable[Completion]:
         """
         Complete dictionary keys.
@@ -478,6 +477,7 @@ class DictionaryCompleter(Completer):
 
         def meta_repr(value: object) -> Callable[[], str]:
             "Abbreviate meta text, make sure it fits on one line."
+
             # We return a function, so that it gets computed when it's needed.
             # When there are many completions, that improves the performance
             # quite a bit (for the multi-column completion menu, we only need
@@ -549,7 +549,7 @@ class DictionaryCompleter(Completer):
         self,
         document: Document,
         complete_event: CompleteEvent,
-        temp_locals: Dict[str, Any],
+        temp_locals: dict[str, Any],
     ) -> Iterable[Completion]:
         """
         Complete attribute names.
@@ -568,9 +568,9 @@ class DictionaryCompleter(Completer):
                     obj = getattr(result, name, None)
                     if inspect.isfunction(obj) or inspect.ismethod(obj):
                         return "()"
-                    if isinstance(obj, dict):
+                    if isinstance(obj, collections_abc.Mapping):
                         return "{}"
-                    if isinstance(obj, (list, tuple)):
+                    if isinstance(obj, collections_abc.Sequence):
                         return "[]"
                 except:
                     pass
@@ -581,13 +581,13 @@ class DictionaryCompleter(Completer):
                     suffix = get_suffix(name)
                     yield Completion(name, -len(attr_name), display=name + suffix)
 
-    def _sort_attribute_names(self, names: List[str]) -> List[str]:
+    def _sort_attribute_names(self, names: list[str]) -> list[str]:
         """
         Sort attribute names alphabetically, but move the double underscore and
         underscore names to the end.
         """
 
-        def sort_key(name: str) -> Tuple[int, str]:
+        def sort_key(name: str) -> tuple[int, str]:
             if name.startswith("__"):
                 return (2, name)  # Double underscore comes latest.
             if name.startswith("_"):
@@ -599,7 +599,7 @@ class DictionaryCompleter(Completer):
 
 class HidePrivateCompleter(Completer):
     """
-    Wrapper around completer that hides private fields, deponding on whether or
+    Wrapper around completer that hides private fields, depending on whether or
     not public fields are shown.
 
     (The reason this is implemented as a `Completer` wrapper is because this
@@ -617,7 +617,6 @@ class HidePrivateCompleter(Completer):
     def get_completions(
         self, document: Document, complete_event: CompleteEvent
     ) -> Iterable[Completion]:
-
         completions = list(self.completer.get_completions(document, complete_event))
         complete_private_attributes = self.complete_private_attributes()
         hide_private = False
@@ -653,7 +652,7 @@ except ImportError:  # Python 2.
 
 
 def _get_style_for_jedi_completion(
-    jedi_completion: "jedi.api.classes.Completion",
+    jedi_completion: jedi.api.classes.Completion,
 ) -> str:
     """
     Return completion style to use for this name.
diff --git a/ptpython/contrib/asyncssh_repl.py b/ptpython/contrib/asyncssh_repl.py
index 4c36217..0347ade 100644
--- a/ptpython/contrib/asyncssh_repl.py
+++ b/ptpython/contrib/asyncssh_repl.py
@@ -6,6 +6,8 @@ Note that the code in this file is Python 3 only. However, we
 should make sure not to use Python 3-only syntax, because this
 package should be installable in Python 2 as well!
 """
+from __future__ import annotations
+
 import asyncio
 from typing import Any, Optional, TextIO, cast
 
@@ -29,7 +31,7 @@ class ReplSSHServerSession(asyncssh.SSHServerSession):
     """
 
     def __init__(
-        self, get_globals: _GetNamespace, get_locals: Optional[_GetNamespace] = None
+        self, get_globals: _GetNamespace, get_locals: _GetNamespace | None = None
     ) -> None:
         self._chan: Any = None
 
diff --git a/ptpython/entry_points/run_ptipython.py b/ptpython/entry_points/run_ptipython.py
index 21d7063..b660a0a 100644
--- a/ptpython/entry_points/run_ptipython.py
+++ b/ptpython/entry_points/run_ptipython.py
@@ -1,4 +1,6 @@
 #!/usr/bin/env python
+from __future__ import annotations
+
 import os
 import sys
 
@@ -58,7 +60,7 @@ def run(user_ns=None):
                     code = compile(f.read(), path, "exec")
                     exec(code, user_ns, user_ns)
             else:
-                print("File not found: {}\n\n".format(path))
+                print(f"File not found: {path}\n\n")
                 sys.exit(1)
 
         # Apply config file
diff --git a/ptpython/entry_points/run_ptpython.py b/ptpython/entry_points/run_ptpython.py
index edffa44..1b4074d 100644
--- a/ptpython/entry_points/run_ptpython.py
+++ b/ptpython/entry_points/run_ptpython.py
@@ -21,6 +21,8 @@ environment variables:
   PTPYTHON_CONFIG_HOME: a configuration directory to use
   PYTHONSTARTUP: file executed on interactive startup (no default)
 """
+from __future__ import annotations
+
 import argparse
 import os
 import pathlib
@@ -44,7 +46,7 @@ __all__ = ["create_parser", "get_config_and_history_file", "run"]
 
 
 class _Parser(argparse.ArgumentParser):
-    def print_help(self, file: Optional[IO[str]] = None) -> None:
+    def print_help(self, file: IO[str] | None = None) -> None:
         super().print_help()
         print(
             dedent(
@@ -90,7 +92,7 @@ def create_parser() -> _Parser:
     return parser
 
 
-def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str]:
+def get_config_and_history_file(namespace: argparse.Namespace) -> tuple[str, str]:
     """
     Check which config/history files to use, ensure that the directories for
     these files exist, and return the config and history path.
diff --git a/ptpython/eventloop.py b/ptpython/eventloop.py
index 63dd740..14ab64b 100644
--- a/ptpython/eventloop.py
+++ b/ptpython/eventloop.py
@@ -7,6 +7,8 @@ way we don't block the UI of for instance ``turtle`` and other Tk libraries.
 in readline. ``prompt-toolkit`` doesn't understand that input hook, but this
 will fix it for Tk.)
 """
+from __future__ import annotations
+
 import sys
 import time
 
diff --git a/ptpython/filters.py b/ptpython/filters.py
index be85edf..a2079fd 100644
--- a/ptpython/filters.py
+++ b/ptpython/filters.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from typing import TYPE_CHECKING
 
 from prompt_toolkit.filters import Filter
@@ -9,7 +11,7 @@ __all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"]
 
 
 class PythonInputFilter(Filter):
-    def __init__(self, python_input: "PythonInput") -> None:
+    def __init__(self, python_input: PythonInput) -> None:
         super().__init__()
         self.python_input = python_input
 
diff --git a/ptpython/history_browser.py b/ptpython/history_browser.py
index 08725ee..eea81c2 100644
--- a/ptpython/history_browser.py
+++ b/ptpython/history_browser.py
@@ -4,6 +4,8 @@ Utility to easily select lines from the history and execute them again.
 `create_history_application` creates an `Application` instance that runs will
 run as a sub application of the Repl/PythonInput.
 """
+from __future__ import annotations
+
 from functools import partial
 from typing import TYPE_CHECKING, Callable, List, Optional, Set
 
@@ -128,7 +130,7 @@ class HistoryLayout:
     application.
     """
 
-    def __init__(self, history: "PythonHistory") -> None:
+    def __init__(self, history: PythonHistory) -> None:
         search_toolbar = SearchToolbar()
 
         self.help_buffer_control = BufferControl(
@@ -224,7 +226,7 @@ def _get_top_toolbar_fragments() -> StyleAndTextTuples:
     return [("class:status-bar.title", "History browser - Insert from history")]
 
 
-def _get_bottom_toolbar_fragments(history: "PythonHistory") -> StyleAndTextTuples:
+def _get_bottom_toolbar_fragments(history: PythonHistory) -> StyleAndTextTuples:
     python_input = history.python_input
 
     @if_mousedown
@@ -258,7 +260,7 @@ class HistoryMargin(Margin):
     This displays a green bar for the selected entries.
     """
 
-    def __init__(self, history: "PythonHistory") -> None:
+    def __init__(self, history: PythonHistory) -> None:
         self.history_buffer = history.history_buffer
         self.history_mapping = history.history_mapping
 
@@ -307,7 +309,7 @@ class ResultMargin(Margin):
     The margin to be shown in the result pane.
     """
 
-    def __init__(self, history: "PythonHistory") -> None:
+    def __init__(self, history: PythonHistory) -> None:
         self.history_mapping = history.history_mapping
         self.history_buffer = history.history_buffer
 
@@ -356,7 +358,7 @@ class GrayExistingText(Processor):
     Turn the existing input, before and after the inserted code gray.
     """
 
-    def __init__(self, history_mapping: "HistoryMapping") -> None:
+    def __init__(self, history_mapping: HistoryMapping) -> None:
         self.history_mapping = history_mapping
         self._lines_before = len(
             history_mapping.original_document.text_before_cursor.splitlines()
@@ -384,7 +386,7 @@ class HistoryMapping:
 
     def __init__(
         self,
-        history: "PythonHistory",
+        history: PythonHistory,
         python_history: History,
         original_document: Document,
     ) -> None:
@@ -393,11 +395,11 @@ class HistoryMapping:
         self.original_document = original_document
 
         self.lines_starting_new_entries = set()
-        self.selected_lines: Set[int] = set()
+        self.selected_lines: set[int] = set()
 
         # Process history.
         history_strings = python_history.get_strings()
-        history_lines: List[str] = []
+        history_lines: list[str] = []
 
         for entry_nr, entry in list(enumerate(history_strings))[-HISTORY_COUNT:]:
             self.lines_starting_new_entries.add(len(history_lines))
@@ -419,7 +421,7 @@ class HistoryMapping:
         else:
             self.result_line_offset = 0
 
-    def get_new_document(self, cursor_pos: Optional[int] = None) -> Document:
+    def get_new_document(self, cursor_pos: int | None = None) -> Document:
         """
         Create a `Document` instance that contains the resulting text.
         """
@@ -449,7 +451,7 @@ class HistoryMapping:
         b.set_document(self.get_new_document(b.cursor_position), bypass_readonly=True)
 
 
-def _toggle_help(history: "PythonHistory") -> None:
+def _toggle_help(history: PythonHistory) -> None:
     "Display/hide help."
     help_buffer_control = history.history_layout.help_buffer_control
 
@@ -459,7 +461,7 @@ def _toggle_help(history: "PythonHistory") -> None:
         history.app.layout.current_control = help_buffer_control
 
 
-def _select_other_window(history: "PythonHistory") -> None:
+def _select_other_window(history: PythonHistory) -> None:
     "Toggle focus between left/right window."
     current_buffer = history.app.current_buffer
     layout = history.history_layout.layout
@@ -472,8 +474,8 @@ def _select_other_window(history: "PythonHistory") -> None:
 
 
 def create_key_bindings(
-    history: "PythonHistory",
-    python_input: "PythonInput",
+    history: PythonHistory,
+    python_input: PythonInput,
     history_mapping: HistoryMapping,
 ) -> KeyBindings:
     """
@@ -592,14 +594,12 @@ def create_key_bindings(
 
 
 class PythonHistory:
-    def __init__(
-        self, python_input: "PythonInput", original_document: Document
-    ) -> None:
+    def __init__(self, python_input: PythonInput, original_document: Document) -> None:
         """
         Create an `Application` for the history screen.
         This has to be run as a sub application of `python_input`.
 
-        When this application runs and returns, it retuns the selected lines.
+        When this application runs and returns, it returns the selected lines.
         """
         self.python_input = python_input
 
diff --git a/ptpython/ipython.py b/ptpython/ipython.py
index db2a204..fb4b5ed 100644
--- a/ptpython/ipython.py
+++ b/ptpython/ipython.py
@@ -8,6 +8,8 @@ also the power of for instance all the %-magic functions that IPython has to
 offer.
 
 """
+from __future__ import annotations
+
 from typing import Iterable
 from warnings import warn
 
@@ -62,12 +64,12 @@ class IPythonPrompt(PromptStyle):
 
 class IPythonValidator(PythonValidator):
     def __init__(self, *args, **kwargs):
-        super(IPythonValidator, self).__init__(*args, **kwargs)
+        super().__init__(*args, **kwargs)
         self.isp = IPythonInputSplitter()
 
     def validate(self, document: Document) -> None:
         document = Document(text=self.isp.transform_cell(document.text))
-        super(IPythonValidator, self).validate(document)
+        super().validate(document)
 
 
 def create_ipython_grammar():
diff --git a/ptpython/key_bindings.py b/ptpython/key_bindings.py
index 147a321..d7bb575 100644
--- a/ptpython/key_bindings.py
+++ b/ptpython/key_bindings.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from typing import TYPE_CHECKING
 
 from prompt_toolkit.application import get_app
@@ -47,7 +49,7 @@ def tab_should_insert_whitespace() -> bool:
     return bool(b.text and (not before_cursor or before_cursor.isspace()))
 
 
-def load_python_bindings(python_input: "PythonInput") -> KeyBindings:
+def load_python_bindings(python_input: PythonInput) -> KeyBindings:
     """
     Custom key bindings.
     """
@@ -157,7 +159,7 @@ def load_python_bindings(python_input: "PythonInput") -> KeyBindings:
         Behaviour of the Enter key.
 
         Auto indent after newline/Enter.
-        (When not in Vi navigaton mode, and when multiline is enabled.)
+        (When not in Vi navigation mode, and when multiline is enabled.)
         """
         b = event.current_buffer
         empty_lines_required = python_input.accept_input_on_enter or 10000
@@ -218,7 +220,7 @@ def load_python_bindings(python_input: "PythonInput") -> KeyBindings:
     return bindings
 
 
-def load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings:
+def load_sidebar_bindings(python_input: PythonInput) -> KeyBindings:
     """
     Load bindings for the navigation in the sidebar.
     """
@@ -273,7 +275,7 @@ def load_sidebar_bindings(python_input: "PythonInput") -> KeyBindings:
     return bindings
 
 
-def load_confirm_exit_bindings(python_input: "PythonInput") -> KeyBindings:
+def load_confirm_exit_bindings(python_input: PythonInput) -> KeyBindings:
     """
     Handle yes/no key presses when the exit confirmation is shown.
     """
diff --git a/ptpython/layout.py b/ptpython/layout.py
index 365f381..d15e52e 100644
--- a/ptpython/layout.py
+++ b/ptpython/layout.py
@@ -1,6 +1,8 @@
 """
 Creation of the `Layout` instance for the Python input/REPL.
 """
+from __future__ import annotations
+
 import platform
 import sys
 from enum import Enum
@@ -78,26 +80,26 @@ class CompletionVisualisation(Enum):
     TOOLBAR = "toolbar"
 
 
-def show_completions_toolbar(python_input: "PythonInput") -> Condition:
+def show_completions_toolbar(python_input: PythonInput) -> Condition:
     return Condition(
         lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR
     )
 
 
-def show_completions_menu(python_input: "PythonInput") -> Condition:
+def show_completions_menu(python_input: PythonInput) -> Condition:
     return Condition(
         lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP
     )
 
 
-def show_multi_column_completions_menu(python_input: "PythonInput") -> Condition:
+def show_multi_column_completions_menu(python_input: PythonInput) -> Condition:
     return Condition(
         lambda: python_input.completion_visualisation
         == CompletionVisualisation.MULTI_COLUMN
     )
 
 
-def python_sidebar(python_input: "PythonInput") -> Window:
+def python_sidebar(python_input: PythonInput) -> Window:
     """
     Create the `Layout` for the sidebar with the configurable options.
     """
@@ -105,7 +107,7 @@ def python_sidebar(python_input: "PythonInput") -> Window:
     def get_text_fragments() -> StyleAndTextTuples:
         tokens: StyleAndTextTuples = []
 
-        def append_category(category: "OptionCategory[Any]") -> None:
+        def append_category(category: OptionCategory[Any]) -> None:
             tokens.extend(
                 [
                     ("class:sidebar", "  "),
@@ -149,7 +151,7 @@ def python_sidebar(python_input: "PythonInput") -> Window:
             append_category(category)
 
             for option in category.options:
-                append(i, option.title, "%s" % option.get_current_value())
+                append(i, option.title, "%s" % (option.get_current_value(),))
                 i += 1
 
         tokens.pop()  # Remove last newline.
@@ -172,7 +174,7 @@ def python_sidebar(python_input: "PythonInput") -> Window:
     )
 
 
-def python_sidebar_navigation(python_input: "PythonInput") -> Window:
+def python_sidebar_navigation(python_input: PythonInput) -> Window:
     """
     Create the `Layout` showing the navigation information for the sidebar.
     """
@@ -198,7 +200,7 @@ def python_sidebar_navigation(python_input: "PythonInput") -> Window:
     )
 
 
-def python_sidebar_help(python_input: "PythonInput") -> Container:
+def python_sidebar_help(python_input: PythonInput) -> Container:
     """
     Create the `Layout` for the help text for the current item in the sidebar.
     """
@@ -232,7 +234,7 @@ def python_sidebar_help(python_input: "PythonInput") -> Container:
     )
 
 
-def signature_toolbar(python_input: "PythonInput") -> Container:
+def signature_toolbar(python_input: PythonInput) -> Container:
     """
     Return the `Layout` for the signature.
     """
@@ -318,7 +320,7 @@ class PythonPromptMargin(PromptMargin):
     It shows something like "In [1]:".
     """
 
-    def __init__(self, python_input: "PythonInput") -> None:
+    def __init__(self, python_input: PythonInput) -> None:
         self.python_input = python_input
 
         def get_prompt_style() -> PromptStyle:
@@ -339,7 +341,7 @@ class PythonPromptMargin(PromptMargin):
         super().__init__(get_prompt, get_continuation)
 
 
-def status_bar(python_input: "PythonInput") -> Container:
+def status_bar(python_input: PythonInput) -> Container:
     """
     Create the `Layout` for the status bar.
     """
@@ -412,7 +414,7 @@ def status_bar(python_input: "PythonInput") -> Container:
     )
 
 
-def get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples:
+def get_inputmode_fragments(python_input: PythonInput) -> StyleAndTextTuples:
     """
     Return current input mode as a list of (token, text) tuples for use in a
     toolbar.
@@ -440,7 +442,7 @@ def get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples:
         recording_register = app.vi_state.recording_register
         if recording_register:
             append((token, " "))
-            append((token + " class:record", "RECORD({})".format(recording_register)))
+            append((token + " class:record", f"RECORD({recording_register})"))
             append((token, " - "))
 
         if app.current_buffer.selection_state is not None:
@@ -473,7 +475,7 @@ def get_inputmode_fragments(python_input: "PythonInput") -> StyleAndTextTuples:
     return result
 
 
-def show_sidebar_button_info(python_input: "PythonInput") -> Container:
+def show_sidebar_button_info(python_input: PythonInput) -> Container:
     """
     Create `Layout` for the information in the right-bottom corner.
     (The right part of the status bar.)
@@ -519,7 +521,7 @@ def show_sidebar_button_info(python_input: "PythonInput") -> Container:
 
 
 def create_exit_confirmation(
-    python_input: "PythonInput", style: str = "class:exit-confirmation"
+    python_input: PythonInput, style: str = "class:exit-confirmation"
 ) -> Container:
     """
     Create `Layout` for the exit message.
@@ -543,7 +545,7 @@ def create_exit_confirmation(
     )
 
 
-def meta_enter_message(python_input: "PythonInput") -> Container:
+def meta_enter_message(python_input: PythonInput) -> Container:
     """
     Create the `Layout` for the 'Meta+Enter` message.
     """
@@ -575,15 +577,15 @@ def meta_enter_message(python_input: "PythonInput") -> Container:
 class PtPythonLayout:
     def __init__(
         self,
-        python_input: "PythonInput",
+        python_input: PythonInput,
         lexer: Lexer,
-        extra_body: Optional[AnyContainer] = None,
-        extra_toolbars: Optional[List[AnyContainer]] = None,
-        extra_buffer_processors: Optional[List[Processor]] = None,
-        input_buffer_height: Optional[AnyDimension] = None,
+        extra_body: AnyContainer | None = None,
+        extra_toolbars: list[AnyContainer] | None = None,
+        extra_buffer_processors: list[Processor] | None = None,
+        input_buffer_height: AnyDimension | None = None,
     ) -> None:
         D = Dimension
-        extra_body_list: List[AnyContainer] = [extra_body] if extra_body else []
+        extra_body_list: list[AnyContainer] = [extra_body] if extra_body else []
         extra_toolbars = extra_toolbars or []
 
         input_buffer_height = input_buffer_height or D(min=6)
@@ -591,7 +593,7 @@ class PtPythonLayout:
         search_toolbar = SearchToolbar(python_input.search_buffer)
 
         def create_python_input_window() -> Window:
-            def menu_position() -> Optional[int]:
+            def menu_position() -> int | None:
                 """
                 When there is no autocompletion menu to be shown, and we have a
                 signature, set the pop-up position at `bracket_start`.
diff --git a/ptpython/lexer.py b/ptpython/lexer.py
index 62e470f..81924c9 100644
--- a/ptpython/lexer.py
+++ b/ptpython/lexer.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from typing import Callable, Optional
 
 from prompt_toolkit.document import Document
@@ -17,7 +19,7 @@ class PtpythonLexer(Lexer):
     use a Python 3 lexer.
     """
 
-    def __init__(self, python_lexer: Optional[Lexer] = None) -> None:
+    def __init__(self, python_lexer: Lexer | None = None) -> None:
         self.python_lexer = python_lexer or PygmentsLexer(PythonLexer)
         self.system_lexer = PygmentsLexer(BashLexer)
 
diff --git a/ptpython/prompt_style.py b/ptpython/prompt_style.py
index e7334af..96b738f 100644
--- a/ptpython/prompt_style.py
+++ b/ptpython/prompt_style.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from abc import ABCMeta, abstractmethod
 from typing import TYPE_CHECKING
 
@@ -40,7 +42,7 @@ class IPythonPrompt(PromptStyle):
     A prompt resembling the IPython prompt.
     """
 
-    def __init__(self, python_input: "PythonInput") -> None:
+    def __init__(self, python_input: PythonInput) -> None:
         self.python_input = python_input
 
     def in_prompt(self) -> AnyFormattedText:
diff --git a/ptpython/python_input.py b/ptpython/python_input.py
index c561117..da19076 100644
--- a/ptpython/python_input.py
+++ b/ptpython/python_input.py
@@ -2,7 +2,7 @@
 Application for reading Python input.
 This can be used for creation of Python REPLs.
 """
-import __future__
+from __future__ import annotations
 
 from asyncio import get_event_loop
 from functools import partial
@@ -34,6 +34,12 @@ from prompt_toolkit.completion import (
     ThreadedCompleter,
     merge_completers,
 )
+from prompt_toolkit.cursor_shapes import (
+    AnyCursorShapeConfig,
+    CursorShape,
+    DynamicCursorShapeConfig,
+    ModalCursorShapeConfig,
+)
 from prompt_toolkit.document import Document
 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
 from prompt_toolkit.filters import Condition
@@ -84,6 +90,11 @@ from .style import generate_style, get_all_code_styles, get_all_ui_styles
 from .utils import unindent_code
 from .validator import PythonValidator
 
+# Isort introduces a SyntaxError, if we'd write `import __future__`.
+# https://github.com/PyCQA/isort/issues/2100
+__future__ = __import__("__future__")
+
+
 __all__ = ["PythonInput"]
 
 
@@ -101,7 +112,7 @@ _T = TypeVar("_T", bound="_SupportsLessThan")
 
 
 class OptionCategory(Generic[_T]):
-    def __init__(self, title: str, options: List["Option[_T]"]) -> None:
+    def __init__(self, title: str, options: list[Option[_T]]) -> None:
         self.title = title
         self.options = options
 
@@ -194,26 +205,25 @@ class PythonInput:
 
     def __init__(
         self,
-        get_globals: Optional[_GetNamespace] = None,
-        get_locals: Optional[_GetNamespace] = None,
-        history_filename: Optional[str] = None,
+        get_globals: _GetNamespace | None = None,
+        get_locals: _GetNamespace | None = None,
+        history_filename: str | None = None,
         vi_mode: bool = False,
-        color_depth: Optional[ColorDepth] = None,
+        color_depth: ColorDepth | None = None,
         # Input/output.
-        input: Optional[Input] = None,
-        output: Optional[Output] = None,
+        input: Input | None = None,
+        output: Output | None = None,
         # For internal use.
-        extra_key_bindings: Optional[KeyBindings] = None,
+        extra_key_bindings: KeyBindings | None = None,
         create_app: bool = True,
-        _completer: Optional[Completer] = None,
-        _validator: Optional[Validator] = None,
-        _lexer: Optional[Lexer] = None,
+        _completer: Completer | None = None,
+        _validator: Validator | None = None,
+        _lexer: Lexer | None = None,
         _extra_buffer_processors=None,
-        _extra_layout_body: Optional[AnyContainer] = None,
+        _extra_layout_body: AnyContainer | None = None,
         _extra_toolbars=None,
         _input_buffer_height=None,
     ) -> None:
-
         self.get_globals: _GetNamespace = get_globals or (lambda: {})
         self.get_locals: _GetNamespace = get_locals or self.get_globals
 
@@ -310,7 +320,7 @@ class PythonInput:
         self.show_exit_confirmation: bool = False
 
         # The title to be displayed in the terminal. (None or string.)
-        self.terminal_title: Optional[str] = None
+        self.terminal_title: str | None = None
 
         self.exit_message: str = "Do you really want to exit?"
         self.insert_blank_line_after_output: bool = True  # (For the REPL.)
@@ -321,11 +331,23 @@ class PythonInput:
         self.search_buffer: Buffer = Buffer()
         self.docstring_buffer: Buffer = Buffer(read_only=True)
 
+        # Cursor shapes.
+        self.cursor_shape_config = "Block"
+        self.all_cursor_shape_configs: Dict[str, AnyCursorShapeConfig] = {
+            "Block": CursorShape.BLOCK,
+            "Underline": CursorShape.UNDERLINE,
+            "Beam": CursorShape.BEAM,
+            "Modal (vi)": ModalCursorShapeConfig(),
+            "Blink block": CursorShape.BLINKING_BLOCK,
+            "Blink under": CursorShape.BLINKING_UNDERLINE,
+            "Blink beam": CursorShape.BLINKING_BEAM,
+        }
+
         # Tokens to be shown at the prompt.
         self.prompt_style: str = "classic"  # The currently active style.
 
         # Styles selectable from the menu.
-        self.all_prompt_styles: Dict[str, PromptStyle] = {
+        self.all_prompt_styles: dict[str, PromptStyle] = {
             "ipython": IPythonPrompt(self),
             "classic": ClassicPrompt(),
         }
@@ -339,7 +361,7 @@ class PythonInput:
         ].out_prompt()
 
         #: Load styles.
-        self.code_styles: Dict[str, BaseStyle] = get_all_code_styles()
+        self.code_styles: dict[str, BaseStyle] = get_all_code_styles()
         self.ui_styles = get_all_ui_styles()
         self._current_code_style_name: str = "default"
         self._current_ui_style_name: str = "default"
@@ -361,7 +383,7 @@ class PythonInput:
         self.current_statement_index: int = 1
 
         # Code signatures. (This is set asynchronously after a timeout.)
-        self.signatures: List[Signature] = []
+        self.signatures: list[Signature] = []
 
         # Boolean indicating whether we have a signatures thread running.
         # (Never run more than one at the same time.)
@@ -400,9 +422,7 @@ class PythonInput:
         # Create an app if requested. If not, the global get_app() is returned
         # for self.app via property getter.
         if create_app:
-            self._app: Optional[Application[str]] = self._create_application(
-                input, output
-            )
+            self._app: Application[str] | None = self._create_application(input, output)
             # Setting vi_mode will not work unless the prompt_toolkit
             # application has been created.
             if vi_mode:
@@ -528,7 +548,7 @@ class PythonInput:
             self.ui_styles[self._current_ui_style_name],
         )
 
-    def _create_options(self) -> List[OptionCategory[Any]]:
+    def _create_options(self) -> list[OptionCategory[Any]]:
         """
         Create a list of `Option` instances for the options sidebar.
         """
@@ -547,14 +567,14 @@ class PythonInput:
             title: str,
             description: str,
             field_name: str,
-            values: Tuple[str, str] = ("off", "on"),
+            values: tuple[str, str] = ("off", "on"),
         ) -> Option[str]:
             "Create Simple on/of option."
 
             def get_current_value() -> str:
                 return values[bool(getattr(self, field_name))]
 
-            def get_values() -> Dict[str, Callable[[], bool]]:
+            def get_values() -> dict[str, Callable[[], bool]]:
                 return {
                     values[1]: lambda: enable(field_name),
                     values[0]: lambda: disable(field_name),
@@ -582,6 +602,16 @@ class PythonInput:
                             "Vi": lambda: enable("vi_mode"),
                         },
                     ),
+                    Option(
+                        title="Cursor shape",
+                        description="Change the cursor style, possibly according "
+                        "to the Vi input mode.",
+                        get_current_value=lambda: self.cursor_shape_config,
+                        get_values=lambda: dict(
+                            (s, partial(enable, "cursor_shape_config", s))
+                            for s in self.all_cursor_shape_configs
+                        ),
+                    ),
                     simple_option(
                         title="Paste mode",
                         description="When enabled, don't indent automatically.",
@@ -731,10 +761,10 @@ class PythonInput:
                         title="Prompt",
                         description="Visualisation of the prompt. ('>>>' or 'In [1]:')",
                         get_current_value=lambda: self.prompt_style,
-                        get_values=lambda: dict(
-                            (s, partial(enable, "prompt_style", s))
+                        get_values=lambda: {
+                            s: partial(enable, "prompt_style", s)
                             for s in self.all_prompt_styles
-                        ),
+                        },
                     ),
                     simple_option(
                         title="Blank line after input",
@@ -826,10 +856,10 @@ class PythonInput:
                         title="User interface",
                         description="Color scheme to use for the user interface.",
                         get_current_value=lambda: self._current_ui_style_name,
-                        get_values=lambda: dict(
-                            (name, partial(self.use_ui_colorscheme, name))
+                        get_values=lambda: {
+                            name: partial(self.use_ui_colorscheme, name)
                             for name in self.ui_styles
-                        ),
+                        },
                     ),
                     Option(
                         title="Color depth",
@@ -863,7 +893,7 @@ class PythonInput:
         ]
 
     def _create_application(
-        self, input: Optional[Input], output: Optional[Output]
+        self, input: Input | None, output: Output | None
     ) -> Application[str]:
         """
         Create an `Application` instance.
@@ -894,6 +924,9 @@ class PythonInput:
             style_transformation=self.style_transformation,
             include_default_pygments_style=False,
             reverse_vi_search_direction=True,
+            cursor=DynamicCursorShapeConfig(
+                lambda: self.all_cursor_shape_configs[self.cursor_shape_config]
+            ),
             input=input,
             output=output,
         )
@@ -953,7 +986,7 @@ class PythonInput:
         in another thread, get the signature of the current code.
         """
 
-        def get_signatures_in_executor(document: Document) -> List[Signature]:
+        def get_signatures_in_executor(document: Document) -> list[Signature]:
             # First, get signatures from Jedi. If we didn't found any and if
             # "dictionary completion" (eval-based completion) is enabled, then
             # get signatures using eval.
@@ -1043,6 +1076,7 @@ class PythonInput:
 
         This can raise EOFError, when Control-D is pressed.
         """
+
         # Capture the current input_mode in order to restore it after reset,
         # for ViState.reset() sets it to InputMode.INSERT unconditionally and
         # doesn't accept any arguments.
diff --git a/ptpython/repl.py b/ptpython/repl.py
index 3c729c0..02a5075 100644
--- a/ptpython/repl.py
+++ b/ptpython/repl.py
@@ -7,6 +7,8 @@ Utility for creating a Python repl.
     embed(globals(), locals(), vi_mode=False)
 
 """
+from __future__ import annotations
+
 import asyncio
 import builtins
 import os
@@ -53,7 +55,7 @@ except ImportError:
 __all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"]
 
 
-def _get_coroutine_flag() -> Optional[int]:
+def _get_coroutine_flag() -> int | None:
     for k, v in COMPILER_FLAG_NAMES.items():
         if v == "COROUTINE":
             return k
@@ -62,7 +64,7 @@ def _get_coroutine_flag() -> Optional[int]:
     return None
 
 
-COROUTINE_FLAG: Optional[int] = _get_coroutine_flag()
+COROUTINE_FLAG: int | None = _get_coroutine_flag()
 
 
 def _has_coroutine_flag(code: types.CodeType) -> bool:
@@ -89,14 +91,15 @@ class PythonRepl(PythonInput):
                         exec(code, self.get_globals(), self.get_locals())
                 else:
                     output = self.app.output
-                    output.write("WARNING | File not found: {}\n\n".format(path))
+                    output.write(f"WARNING | File not found: {path}\n\n")
 
     def run_and_show_expression(self, expression: str) -> None:
         try:
             # Eval.
             try:
                 result = self.eval(expression)
-            except KeyboardInterrupt:  # KeyboardInterrupt doesn't inherit from Exception.
+            except KeyboardInterrupt:
+                # KeyboardInterrupt doesn't inherit from Exception.
                 raise
             except SystemExit:
                 raise
@@ -299,7 +302,7 @@ class PythonRepl(PythonInput):
         return None
 
     def _store_eval_result(self, result: object) -> None:
-        locals: Dict[str, Any] = self.get_locals()
+        locals: dict[str, Any] = self.get_locals()
         locals["_"] = locals["_%i" % self.current_statement_index] = result
 
     def get_compiler_flags(self) -> int:
@@ -402,7 +405,7 @@ class PythonRepl(PythonInput):
 
     def show_result(self, result: object) -> None:
         """
-        Show __repr__ for an `eval` result and print to ouptut.
+        Show __repr__ for an `eval` result and print to output.
         """
         formatted_text_output = self._format_result_output(result)
 
@@ -523,7 +526,7 @@ class PythonRepl(PythonInput):
 
         flush_page()
 
-    def create_pager_prompt(self) -> PromptSession["PagerResult"]:
+    def create_pager_prompt(self) -> PromptSession[PagerResult]:
         """
         Create pager --MORE-- prompt.
         """
@@ -572,8 +575,6 @@ class PythonRepl(PythonInput):
             include_default_pygments_style=False,
             output=output,
         )
-
-        output.write("%s\n" % e)
         output.flush()
 
     def _handle_keyboard_interrupt(self, e: KeyboardInterrupt) -> None:
@@ -652,7 +653,7 @@ def run_config(
 
     # Run the config file in an empty namespace.
     try:
-        namespace: Dict[str, Any] = {}
+        namespace: dict[str, Any] = {}
 
         with open(config_file, "rb") as f:
             code = compile(f.read(), config_file, "exec")
@@ -671,10 +672,10 @@ def run_config(
 def embed(
     globals=None,
     locals=None,
-    configure: Optional[Callable[[PythonRepl], None]] = None,
+    configure: Callable[[PythonRepl], None] | None = None,
     vi_mode: bool = False,
-    history_filename: Optional[str] = None,
-    title: Optional[str] = None,
+    history_filename: str | None = None,
+    title: str | None = None,
     startup_paths=None,
     patch_stdout: bool = False,
     return_asyncio_coroutine: bool = False,
diff --git a/ptpython/signatures.py b/ptpython/signatures.py
index e836d33..5a6f286 100644
--- a/ptpython/signatures.py
+++ b/ptpython/signatures.py
@@ -5,6 +5,8 @@ editing.
 Either with the Jedi library, or using `inspect.signature` if Jedi fails and we
 can use `eval()` to evaluate the function object.
 """
+from __future__ import annotations
+
 import inspect
 from inspect import Signature as InspectSignature
 from inspect import _ParameterKind as ParameterKind
@@ -25,8 +27,8 @@ class Parameter:
     def __init__(
         self,
         name: str,
-        annotation: Optional[str],
-        default: Optional[str],
+        annotation: str | None,
+        default: str | None,
         kind: ParameterKind,
     ) -> None:
         self.name = name
@@ -66,9 +68,9 @@ class Signature:
         name: str,
         docstring: str,
         parameters: Sequence[Parameter],
-        index: Optional[int] = None,
+        index: int | None = None,
         returns: str = "",
-        bracket_start: Tuple[int, int] = (0, 0),
+        bracket_start: tuple[int, int] = (0, 0),
     ) -> None:
         self.name = name
         self.docstring = docstring
@@ -84,7 +86,7 @@ class Signature:
         docstring: str,
         signature: InspectSignature,
         index: int,
-    ) -> "Signature":
+    ) -> Signature:
         parameters = []
 
         def get_annotation_name(annotation: object) -> str:
@@ -123,9 +125,7 @@ class Signature:
         )
 
     @classmethod
-    def from_jedi_signature(
-        cls, signature: "jedi.api.classes.Signature"
-    ) -> "Signature":
+    def from_jedi_signature(cls, signature: jedi.api.classes.Signature) -> Signature:
         parameters = []
 
         for p in signature.params:
@@ -160,8 +160,8 @@ class Signature:
 
 
 def get_signatures_using_jedi(
-    document: Document, locals: Dict[str, Any], globals: Dict[str, Any]
-) -> List[Signature]:
+    document: Document, locals: dict[str, Any], globals: dict[str, Any]
+) -> list[Signature]:
     script = get_jedi_script_from_document(document, locals, globals)
 
     # Show signatures in help text.
@@ -195,8 +195,8 @@ def get_signatures_using_jedi(
 
 
 def get_signatures_using_eval(
-    document: Document, locals: Dict[str, Any], globals: Dict[str, Any]
-) -> List[Signature]:
+    document: Document, locals: dict[str, Any], globals: dict[str, Any]
+) -> list[Signature]:
     """
     Look for the signature of the function before the cursor position without
     use of Jedi. This uses a similar approach as the `DictionaryCompleter` of
diff --git a/ptpython/style.py b/ptpython/style.py
index 4b54d0c..199d5ab 100644
--- a/ptpython/style.py
+++ b/ptpython/style.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from typing import Dict
 
 from prompt_toolkit.styles import BaseStyle, Style, merge_styles
@@ -8,11 +10,11 @@ from pygments.styles import get_all_styles, get_style_by_name
 __all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"]
 
 
-def get_all_code_styles() -> Dict[str, BaseStyle]:
+def get_all_code_styles() -> dict[str, BaseStyle]:
     """
     Return a mapping from style names to their classes.
     """
-    result: Dict[str, BaseStyle] = {
+    result: dict[str, BaseStyle] = {
         name: style_from_pygments_cls(get_style_by_name(name))
         for name in get_all_styles()
     }
@@ -20,7 +22,7 @@ def get_all_code_styles() -> Dict[str, BaseStyle]:
     return result
 
 
-def get_all_ui_styles() -> Dict[str, BaseStyle]:
+def get_all_ui_styles() -> dict[str, BaseStyle]:
     """
     Return a dict mapping {ui_style_name -> style_dict}.
     """
diff --git a/ptpython/utils.py b/ptpython/utils.py
index ef96ca4..5348899 100644
--- a/ptpython/utils.py
+++ b/ptpython/utils.py
@@ -1,6 +1,8 @@
 """
 For internal use only.
 """
+from __future__ import annotations
+
 import re
 from typing import (
     TYPE_CHECKING,
@@ -65,8 +67,8 @@ def has_unclosed_brackets(text: str) -> bool:
 
 
 def get_jedi_script_from_document(
-    document: Document, locals: Dict[str, Any], globals: Dict[str, Any]
-) -> "Interpreter":
+    document: Document, locals: dict[str, Any], globals: dict[str, Any]
+) -> Interpreter:
     import jedi  # We keep this import in-line, to improve start-up time.
 
     # Importing Jedi is 'slow'.
@@ -154,7 +156,7 @@ def if_mousedown(handler: _T) -> _T:
     by the Window.)
     """
 
-    def handle_if_mouse_down(mouse_event: MouseEvent) -> "NotImplementedOrNone":
+    def handle_if_mouse_down(mouse_event: MouseEvent) -> NotImplementedOrNone:
         if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
             return handler(mouse_event)
         else:
diff --git a/ptpython/validator.py b/ptpython/validator.py
index ffac583..3b36d27 100644
--- a/ptpython/validator.py
+++ b/ptpython/validator.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from typing import Callable, Optional
 
 from prompt_toolkit.document import Document
@@ -16,7 +18,7 @@ class PythonValidator(Validator):
         active compiler flags.
     """
 
-    def __init__(self, get_compiler_flags: Optional[Callable[[], int]] = None) -> None:
+    def __init__(self, get_compiler_flags: Callable[[], int] | None = None) -> None:
         self.get_compiler_flags = get_compiler_flags
 
     def validate(self, document: Document) -> None:
diff --git a/setup.py b/setup.py
index 2725dac..18d2911 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@ with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f:
 setup(
     name="ptpython",
     author="Jonathan Slenders",
-    version="3.0.22",
+    version="3.0.23",
     url="https://github.com/prompt-toolkit/ptpython",
     description="Python REPL build on top of prompt_toolkit",
     long_description=long_description,
@@ -21,14 +21,14 @@ setup(
         "appdirs",
         "importlib_metadata;python_version<'3.8'",
         "jedi>=0.16.0",
-        # Use prompt_toolkit 3.0.18, because of the `in_thread` option.
-        "prompt_toolkit>=3.0.18,<3.1.0",
+        # Use prompt_toolkit 3.0.28, because of cursor shape support.
+        "prompt_toolkit>=3.0.28,<3.1.0",
         "pygments",
     ],
-    python_requires=">=3.6",
+    python_requires=">=3.7",
     classifiers=[
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3 :: Only",
diff --git a/tests/run_tests.py b/tests/run_tests.py
index 2f94516..0de3743 100755
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -1,4 +1,6 @@
 #!/usr/bin/env python
+from __future__ import annotations
+
 import unittest
 
 import ptpython.completer