Skip to content

Commit ac419e1

Browse files
committedJan 27, 2020
Upgrade to prompt_toolkit 3.0.
- Drop Python <3.5 support and prompt_toolkit 2. - Code formatting with black. - Sorted imports with isort. - Added type annotations. - Separate event loop for reading user input.
1 parent 392b08b commit ac419e1

27 files changed

+2065
-1589
lines changed
 

‎.travis.yml

+7-8
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,17 @@ language: python
44
matrix:
55
include:
66
- python: 3.6
7-
- python: 3.5
8-
- python: 3.4
9-
- python: 3.3
10-
- python: 2.7
11-
- python: 2.6
12-
- python: pypy
13-
- python: pypy3
7+
- python: 3.7
148

159
install:
16-
- travis_retry pip install . pytest
10+
- travis_retry pip install . pytest isort black
1711
- pip list
1812

1913
script:
2014
- echo "$TRAVIS_PYTHON_VERSION"
2115
- ./tests/run_tests.py
16+
17+
# Check wheather the imports were sorted correctly.
18+
- isort -c -rc ptpython tests setup.py examples
19+
20+
- black --check ptpython setup.py examples

‎examples/asyncio-python-embed.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
prompt.
1313
"""
1414
from __future__ import unicode_literals
15-
from ptpython.repl import embed
1615

1716
import asyncio
1817

18+
from ptpython.repl import embed
19+
1920
loop = asyncio.get_event_loop()
2021
counter = [0]
2122

@@ -26,7 +27,7 @@ def print_counter():
2627
Coroutine that prints counters and saves it in a global variable.
2728
"""
2829
while True:
29-
print('Counter: %i' % counter[0])
30+
print("Counter: %i" % counter[0])
3031
counter[0] += 1
3132
yield from asyncio.sleep(3)
3233

@@ -37,9 +38,13 @@ def interactive_shell():
3738
Coroutine that starts a Python REPL from which we can access the global
3839
counter variable.
3940
"""
40-
print('You should be able to read and update the "counter[0]" variable from this shell.')
41+
print(
42+
'You should be able to read and update the "counter[0]" variable from this shell.'
43+
)
4144
try:
42-
yield from embed(globals=globals(), return_asyncio_coroutine=True, patch_stdout=True)
45+
yield from embed(
46+
globals=globals(), return_asyncio_coroutine=True, patch_stdout=True
47+
)
4348
except EOFError:
4449
# Stop the loop when quitting the repl. (Ctrl-D press.)
4550
loop.stop()
@@ -53,5 +58,5 @@ def main():
5358
loop.close()
5459

5560

56-
if __name__ == '__main__':
61+
if __name__ == "__main__":
5762
main()

‎examples/asyncio-ssh-python-embed.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
Run this example and then SSH to localhost, port 8222.
77
"""
88
import asyncio
9-
import asyncssh
109
import logging
1110

11+
import asyncssh
12+
1213
from ptpython.contrib.asyncssh_repl import ReplSSHServerSession
1314

1415
logging.basicConfig()
@@ -19,6 +20,7 @@ class MySSHServer(asyncssh.SSHServer):
1920
"""
2021
Server without authentication, running `ReplSSHServerSession`.
2122
"""
23+
2224
def __init__(self, get_namespace):
2325
self.get_namespace = get_namespace
2426

@@ -37,22 +39,24 @@ def main(port=8222):
3739
loop = asyncio.get_event_loop()
3840

3941
# Namespace exposed in the REPL.
40-
environ = {'hello': 'world'}
42+
environ = {"hello": "world"}
4143

4244
# Start SSH server.
4345
def create_server():
4446
return MySSHServer(lambda: environ)
4547

46-
print('Listening on :%i' % port)
48+
print("Listening on :%i" % port)
4749
print('To connect, do "ssh localhost -p %i"' % port)
4850

4951
loop.run_until_complete(
50-
asyncssh.create_server(create_server, '', port,
51-
server_host_keys=['/etc/ssh/ssh_host_dsa_key']))
52+
asyncssh.create_server(
53+
create_server, "", port, server_host_keys=["/etc/ssh/ssh_host_dsa_key"]
54+
)
55+
)
5256

5357
# Run eventloop.
5458
loop.run_forever()
5559

5660

57-
if __name__ == '__main__':
61+
if __name__ == "__main__":
5862
main()

‎examples/ptpython_config/config.py

+11-14
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@
44
Copy this file to $XDG_CONFIG_HOME/ptpython/config.py
55
"""
66
from __future__ import unicode_literals
7+
78
from prompt_toolkit.filters import ViInsertMode
89
from prompt_toolkit.key_binding.key_processor import KeyPress
910
from prompt_toolkit.keys import Keys
1011
from pygments.token import Token
1112

1213
from ptpython.layout import CompletionVisualisation
1314

14-
__all__ = (
15-
'configure',
16-
)
15+
__all__ = ("configure",)
1716

1817

1918
def configure(repl):
@@ -50,7 +49,7 @@ def configure(repl):
5049

5150
# Swap light/dark colors on or off
5251
repl.swap_light_and_dark = False
53-
52+
5453
# Highlight matching parethesis.
5554
repl.highlight_matching_parenthesis = True
5655

@@ -75,7 +74,7 @@ def configure(repl):
7574
repl.paste_mode = False
7675

7776
# Use the classic prompt. (Display '>>>' instead of 'In [1]'.)
78-
repl.prompt_style = 'classic' # 'classic' or 'ipython'
77+
repl.prompt_style = "classic" # 'classic' or 'ipython'
7978

8079
# Don't insert a blank line after the output.
8180
repl.insert_blank_line_after_output = False
@@ -108,14 +107,14 @@ def configure(repl):
108107
repl.enable_input_validation = True
109108

110109
# Use this colorscheme for the code.
111-
repl.use_code_colorscheme('pastie')
110+
repl.use_code_colorscheme("pastie")
112111

113112
# Set color depth (keep in mind that not all terminals support true color).
114113

115-
#repl.color_depth = 'DEPTH_1_BIT' # Monochrome.
116-
#repl.color_depth = 'DEPTH_4_BIT' # ANSI colors only.
117-
repl.color_depth = 'DEPTH_8_BIT' # The default, 256 colors.
118-
#repl.color_depth = 'DEPTH_24_BIT' # True color.
114+
# repl.color_depth = 'DEPTH_1_BIT' # Monochrome.
115+
# repl.color_depth = 'DEPTH_4_BIT' # ANSI colors only.
116+
repl.color_depth = "DEPTH_8_BIT" # The default, 256 colors.
117+
# repl.color_depth = 'DEPTH_24_BIT' # True color.
119118

120119
# Syntax.
121120
repl.enable_syntax_highlighting = True
@@ -142,7 +141,6 @@ def _(event):
142141
event.current_buffer.validate_and_handle()
143142
"""
144143

145-
146144
# Typing 'jj' in Vi Insert mode, should send escape. (Go back to navigation
147145
# mode.)
148146
"""
@@ -178,8 +176,7 @@ def _(event):
178176
# `ptpython/style.py` for all possible tokens.
179177
_custom_ui_colorscheme = {
180178
# Blue prompt.
181-
Token.Layout.Prompt: 'bg:#eeeeff #000000 bold',
182-
179+
Token.Layout.Prompt: "bg:#eeeeff #000000 bold",
183180
# Make the status toolbar red.
184-
Token.Toolbar.Status: 'bg:#ff0000 #000000',
181+
Token.Toolbar.Status: "bg:#ff0000 #000000",
185182
}

‎examples/python-embed-with-custom-prompt.py

+13-14
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
"""
55
from __future__ import unicode_literals
66

7-
from ptpython.repl import embed
8-
from ptpython.prompt_style import PromptStyle
97
from pygments.token import Token
108

9+
from ptpython.prompt_style import PromptStyle
10+
from ptpython.repl import embed
11+
1112

1213
def configure(repl):
1314
# There are several ways to override the prompt.
@@ -18,25 +19,23 @@ def configure(repl):
1819
class CustomPrompt(PromptStyle):
1920
def in_tokens(self, cli):
2021
return [
21-
(Token.In, 'Input['),
22-
(Token.In.Number, '%s' % repl.current_statement_index),
23-
(Token.In, '] >>: '),
22+
(Token.In, "Input["),
23+
(Token.In.Number, "%s" % repl.current_statement_index),
24+
(Token.In, "] >>: "),
2425
]
2526

2627
def in2_tokens(self, cli, width):
27-
return [
28-
(Token.In, '...: '.rjust(width)),
29-
]
28+
return [(Token.In, "...: ".rjust(width))]
3029

3130
def out_tokens(self, cli):
3231
return [
33-
(Token.Out, 'Result['),
34-
(Token.Out.Number, '%s' % repl.current_statement_index),
35-
(Token.Out, ']: '),
32+
(Token.Out, "Result["),
33+
(Token.Out.Number, "%s" % repl.current_statement_index),
34+
(Token.Out, "]: "),
3635
]
3736

38-
repl.all_prompt_styles['custom'] = CustomPrompt()
39-
repl.prompt_style = 'custom'
37+
repl.all_prompt_styles["custom"] = CustomPrompt()
38+
repl.prompt_style = "custom"
4039

4140
# 2. Assign a new callable to `get_input_prompt_tokens`. This will always take effect.
4241
## repl.get_input_prompt_tokens = lambda cli: [(Token.In, '[hello] >>> ')]
@@ -52,5 +51,5 @@ def main():
5251
embed(globals(), locals(), configure=configure)
5352

5453

55-
if __name__ == '__main__':
54+
if __name__ == "__main__":
5655
main()

‎examples/python-embed.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ def main():
1010
embed(globals(), locals(), vi_mode=False)
1111

1212

13-
if __name__ == '__main__':
13+
if __name__ == "__main__":
1414
main()

‎examples/python-input.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ def main():
1010
prompt = PythonInput()
1111

1212
text = prompt.app.run()
13-
print('You said: ' + text)
13+
print("You said: " + text)
1414

1515

16-
if __name__ == '__main__':
16+
if __name__ == "__main__":
1717
main()

‎ptpython/__main__.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""
22
Make `python -m ptpython` an alias for running `./ptpython`.
33
"""
4-
from __future__ import unicode_literals
54
from .entry_points.run_ptpython import run
65

76
run()

‎ptpython/completer.py

+81-61
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1-
from __future__ import unicode_literals
1+
import ast
2+
import keyword
3+
import re
4+
from typing import TYPE_CHECKING, Iterable
25

3-
from prompt_toolkit.completion import Completer, Completion, PathCompleter
6+
from prompt_toolkit.completion import (
7+
CompleteEvent,
8+
Completer,
9+
Completion,
10+
PathCompleter,
11+
)
412
from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar
513
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
14+
from prompt_toolkit.document import Document
615

716
from ptpython.utils import get_jedi_script_from_document
817

9-
import keyword
10-
import ast
11-
import re
12-
import six
18+
if TYPE_CHECKING:
19+
from prompt_toolkit.contrib.regular_languages.compiler import _CompiledGrammar
1320

14-
__all__ = (
15-
'PythonCompleter',
16-
)
21+
__all__ = ["PythonCompleter"]
1722

1823

1924
class PythonCompleter(Completer):
2025
"""
2126
Completer for Python code.
2227
"""
28+
2329
def __init__(self, get_globals, get_locals, get_enable_dictionary_completion):
24-
super(PythonCompleter, self).__init__()
30+
super().__init__()
2531

2632
self.get_globals = get_globals
2733
self.get_locals = get_locals
@@ -33,17 +39,19 @@ def __init__(self, get_globals, get_locals, get_enable_dictionary_completion):
3339
self._path_completer_grammar_cache = None
3440

3541
@property
36-
def _path_completer(self):
42+
def _path_completer(self) -> GrammarCompleter:
3743
if self._path_completer_cache is None:
3844
self._path_completer_cache = GrammarCompleter(
39-
self._path_completer_grammar, {
40-
'var1': PathCompleter(expanduser=True),
41-
'var2': PathCompleter(expanduser=True),
42-
})
45+
self._path_completer_grammar,
46+
{
47+
"var1": PathCompleter(expanduser=True),
48+
"var2": PathCompleter(expanduser=True),
49+
},
50+
)
4351
return self._path_completer_cache
4452

4553
@property
46-
def _path_completer_grammar(self):
54+
def _path_completer_grammar(self) -> "_CompiledGrammar":
4755
"""
4856
Return the grammar for matching paths inside strings inside Python
4957
code.
@@ -54,15 +62,15 @@ def _path_completer_grammar(self):
5462
self._path_completer_grammar_cache = self._create_path_completer_grammar()
5563
return self._path_completer_grammar_cache
5664

57-
def _create_path_completer_grammar(self):
58-
def unwrapper(text):
59-
return re.sub(r'\\(.)', r'\1', text)
65+
def _create_path_completer_grammar(self) -> "_CompiledGrammar":
66+
def unwrapper(text: str) -> str:
67+
return re.sub(r"\\(.)", r"\1", text)
6068

61-
def single_quoted_wrapper(text):
62-
return text.replace('\\', '\\\\').replace("'", "\\'")
69+
def single_quoted_wrapper(text: str) -> str:
70+
return text.replace("\\", "\\\\").replace("'", "\\'")
6371

64-
def double_quoted_wrapper(text):
65-
return text.replace('\\', '\\\\').replace('"', '\\"')
72+
def double_quoted_wrapper(text: str) -> str:
73+
return text.replace("\\", "\\\\").replace('"', '\\"')
6674

6775
grammar = r"""
6876
# Text before the current string.
@@ -91,40 +99,45 @@ def double_quoted_wrapper(text):
9199

92100
return compile_grammar(
93101
grammar,
94-
escape_funcs={
95-
'var1': single_quoted_wrapper,
96-
'var2': double_quoted_wrapper,
97-
},
98-
unescape_funcs={
99-
'var1': unwrapper,
100-
'var2': unwrapper,
101-
})
102-
103-
def _complete_path_while_typing(self, document):
102+
escape_funcs={"var1": single_quoted_wrapper, "var2": double_quoted_wrapper},
103+
unescape_funcs={"var1": unwrapper, "var2": unwrapper},
104+
)
105+
106+
def _complete_path_while_typing(self, document: Document) -> bool:
104107
char_before_cursor = document.char_before_cursor
105-
return document.text and (
106-
char_before_cursor.isalnum() or char_before_cursor in '/.~')
108+
return bool(
109+
document.text
110+
and (char_before_cursor.isalnum() or char_before_cursor in "/.~")
111+
)
107112

108-
def _complete_python_while_typing(self, document):
113+
def _complete_python_while_typing(self, document: Document) -> bool:
109114
char_before_cursor = document.char_before_cursor
110-
return document.text and (
111-
char_before_cursor.isalnum() or char_before_cursor in '_.')
115+
return bool(
116+
document.text
117+
and (char_before_cursor.isalnum() or char_before_cursor in "_.")
118+
)
112119

113-
def get_completions(self, document, complete_event):
120+
def get_completions(
121+
self, document: Document, complete_event: CompleteEvent
122+
) -> Iterable[Completion]:
114123
"""
115124
Get Python completions.
116125
"""
117126
# Do dictionary key completions.
118127
if self.get_enable_dictionary_completion():
119128
has_dict_completions = False
120-
for c in self.dictionary_completer.get_completions(document, complete_event):
129+
for c in self.dictionary_completer.get_completions(
130+
document, complete_event
131+
):
121132
has_dict_completions = True
122133
yield c
123134
if has_dict_completions:
124135
return
125136

126137
# Do Path completions (if there were no dictionary completions).
127-
if complete_event.completion_requested or self._complete_path_while_typing(document):
138+
if complete_event.completion_requested or self._complete_path_while_typing(
139+
document
140+
):
128141
for c in self._path_completer.get_completions(document, complete_event):
129142
yield c
130143

@@ -133,8 +146,12 @@ def get_completions(self, document, complete_event):
133146
return
134147

135148
# Do Jedi Python completions.
136-
if complete_event.completion_requested or self._complete_python_while_typing(document):
137-
script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals())
149+
if complete_event.completion_requested or self._complete_python_while_typing(
150+
document
151+
):
152+
script = get_jedi_script_from_document(
153+
document, self.get_locals(), self.get_globals()
154+
)
138155

139156
if script:
140157
try:
@@ -178,9 +195,11 @@ def get_completions(self, document, complete_event):
178195
else:
179196
for c in completions:
180197
yield Completion(
181-
c.name_with_symbols, len(c.complete) - len(c.name_with_symbols),
198+
c.name_with_symbols,
199+
len(c.complete) - len(c.name_with_symbols),
182200
display=c.name_with_symbols,
183-
style=_get_style_for_name(c.name_with_symbols))
201+
style=_get_style_for_name(c.name_with_symbols),
202+
)
184203

185204

186205
class DictionaryCompleter(Completer):
@@ -191,14 +210,15 @@ class DictionaryCompleter(Completer):
191210
bracket, which is potentially dangerous. It doesn't match on
192211
function calls, so it only triggers attribute access.
193212
"""
213+
194214
def __init__(self, get_globals, get_locals):
195-
super(DictionaryCompleter, self).__init__()
215+
super().__init__()
196216

197217
self.get_globals = get_globals
198218
self.get_locals = get_locals
199219

200220
self.pattern = re.compile(
201-
r'''
221+
r"""
202222
# Any expression safe enough to eval while typing.
203223
# No operators, except dot, and only other dict lookups.
204224
# Technically, this can be unsafe of course, if bad code runs
@@ -227,11 +247,13 @@ def __init__(self, get_globals, get_locals):
227247
# string).
228248
\[
229249
\s* ([a-zA-Z0-9_'"]*)$
230-
''',
231-
re.VERBOSE
250+
""",
251+
re.VERBOSE,
232252
)
233253

234-
def get_completions(self, document, complete_event):
254+
def get_completions(
255+
self, document: Document, complete_event: CompleteEvent
256+
) -> Iterable[Completion]:
235257
match = self.pattern.search(document.text_before_cursor)
236258
if match is not None:
237259
object_var, key = match.groups()
@@ -240,7 +262,7 @@ def get_completions(self, document, complete_event):
240262
# Do lookup of `object_var` in the context.
241263
try:
242264
result = eval(object_var, self.get_globals(), self.get_locals())
243-
except BaseException as e:
265+
except BaseException:
244266
return # Many exception, like NameError can be thrown here.
245267

246268
# If this object is a dictionary, complete the keys.
@@ -256,28 +278,26 @@ def get_completions(self, document, complete_event):
256278
break
257279

258280
for k in result:
259-
if six.text_type(k).startswith(key_obj):
260-
yield Completion(
261-
six.text_type(repr(k)),
262-
- len(key),
263-
display=six.text_type(repr(k))
264-
)
281+
if str(k).startswith(key_obj):
282+
yield Completion(str(repr(k)), -len(key), display=str(repr(k)))
283+
265284

266285
try:
267286
import builtins
287+
268288
_builtin_names = dir(builtins)
269289
except ImportError: # Python 2.
270290
_builtin_names = []
271291

272292

273-
def _get_style_for_name(name):
293+
def _get_style_for_name(name: str) -> str:
274294
"""
275295
Return completion style to use for this name.
276296
"""
277297
if name in _builtin_names:
278-
return 'class:completion.builtin'
298+
return "class:completion.builtin"
279299

280300
if keyword.iskeyword(name):
281-
return 'class:completion.keyword'
301+
return "class:completion.keyword"
282302

283-
return ''
303+
return ""

‎ptpython/contrib/asyncssh_repl.py

+35-49
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,18 @@
66
should make sure not to use Python 3-only syntax, because this
77
package should be installable in Python 2 as well!
88
"""
9-
from __future__ import unicode_literals
10-
119
import asyncio
12-
import asyncssh
10+
from typing import Optional, TextIO, cast
1311

14-
from prompt_toolkit.input import PipeInput
15-
from prompt_toolkit.interface import CommandLineInterface
16-
from prompt_toolkit.layout.screen import Size
17-
from prompt_toolkit.shortcuts import create_asyncio_eventloop
18-
from prompt_toolkit.terminal.vt100_output import Vt100_Output
12+
import asyncssh
13+
from prompt_toolkit.data_structures import Size
14+
from prompt_toolkit.input import create_pipe_input
15+
from prompt_toolkit.output.vt100 import Vt100_Output
1916

17+
from ptpython.python_input import _GetNamespace
2018
from ptpython.repl import PythonRepl
2119

22-
__all__ = (
23-
'ReplSSHServerSession',
24-
)
20+
__all__ = ["ReplSSHServerSession"]
2521

2622

2723
class ReplSSHServerSession(asyncssh.SSHServerSession):
@@ -31,51 +27,47 @@ class ReplSSHServerSession(asyncssh.SSHServerSession):
3127
:param get_globals: callable that returns the current globals.
3228
:param get_locals: (optional) callable that returns the current locals.
3329
"""
34-
def __init__(self, get_globals, get_locals=None):
35-
assert callable(get_globals)
36-
assert get_locals is None or callable(get_locals)
3730

31+
def __init__(
32+
self, get_globals: _GetNamespace, get_locals: Optional[_GetNamespace] = None
33+
) -> None:
3834
self._chan = None
3935

40-
def _globals():
36+
def _globals() -> dict:
4137
data = get_globals()
42-
data.setdefault('print', self._print)
38+
data.setdefault("print", self._print)
4339
return data
4440

45-
repl = PythonRepl(get_globals=_globals,
46-
get_locals=get_locals or _globals)
47-
48-
# Disable open-in-editor and system prompt. Because it would run and
49-
# display these commands on the server side, rather than in the SSH
50-
# client.
51-
repl.enable_open_in_editor = False
52-
repl.enable_system_bindings = False
53-
5441
# PipInput object, for sending input in the CLI.
5542
# (This is something that we can use in the prompt_toolkit event loop,
5643
# but still write date in manually.)
57-
self._input_pipe = PipeInput()
44+
self._input_pipe = create_pipe_input()
5845

5946
# Output object. Don't render to the real stdout, but write everything
6047
# in the SSH channel.
61-
class Stdout(object):
62-
def write(s, data):
48+
class Stdout:
49+
def write(s, data: str) -> None:
6350
if self._chan is not None:
64-
self._chan.write(data.replace('\n', '\r\n'))
51+
data = data.replace("\n", "\r\n")
52+
self._chan.write(data)
6553

66-
def flush(s):
54+
def flush(s) -> None:
6755
pass
6856

69-
# Create command line interface.
70-
self.cli = CommandLineInterface(
71-
application=repl.create_application(),
72-
eventloop=create_asyncio_eventloop(),
57+
self.repl = PythonRepl(
58+
get_globals=_globals,
59+
get_locals=get_locals or _globals,
7360
input=self._input_pipe,
74-
output=Vt100_Output(Stdout(), self._get_size))
61+
output=Vt100_Output(cast(TextIO, Stdout()), self._get_size),
62+
)
7563

76-
self._callbacks = self.cli.create_eventloop_callbacks()
64+
# Disable open-in-editor and system prompt. Because it would run and
65+
# display these commands on the server side, rather than in the SSH
66+
# client.
67+
self.repl.enable_open_in_editor = False
68+
self.repl.enable_system_bindings = False
7769

78-
def _get_size(self):
70+
def _get_size(self) -> Size:
7971
"""
8072
Callable that returns the current `Size`, required by Vt100_Output.
8173
"""
@@ -92,42 +84,36 @@ def connection_made(self, chan):
9284
self._chan = chan
9385

9486
# Run REPL interface.
95-
f = asyncio.ensure_future(self.cli.run_async())
87+
f = asyncio.ensure_future(self.repl.run_async())
9688

9789
# Close channel when done.
98-
def done(_):
90+
def done(_) -> None:
9991
chan.close()
10092
self._chan = None
93+
10194
f.add_done_callback(done)
10295

103-
def shell_requested(self):
96+
def shell_requested(self) -> bool:
10497
return True
10598

10699
def terminal_size_changed(self, width, height, pixwidth, pixheight):
107100
"""
108101
When the terminal size changes, report back to CLI.
109102
"""
110-
self._callbacks.terminal_size_changed()
103+
self.repl.app._on_resize()
111104

112105
def data_received(self, data, datatype):
113106
"""
114107
When data is received, send to inputstream of the CLI and repaint.
115108
"""
116109
self._input_pipe.send(data)
117110

118-
def _print(self, *data, **kw):
111+
def _print(self, *data, sep=" ", end="\n", file=None) -> None:
119112
"""
120-
_print(self, *data, sep=' ', end='\n', file=None)
121-
122113
Alternative 'print' function that prints back into the SSH channel.
123114
"""
124115
# Pop keyword-only arguments. (We cannot use the syntax from the
125116
# signature. Otherwise, Python2 will give a syntax error message when
126117
# installing.)
127-
sep = kw.pop('sep', ' ')
128-
end = kw.pop('end', '\n')
129-
_ = kw.pop('file', None)
130-
assert not kw, 'Too many keyword-only arguments'
131-
132118
data = sep.join(map(str, data))
133119
self._chan.write(data + end)
+32-64
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,37 @@
11
#!/usr/bin/env python
2-
"""
3-
ptipython: IPython interactive shell with the `prompt_toolkit` front-end.
4-
Usage:
5-
ptpython [ --vi ]
6-
[ --config-dir=<directory> ] [ --interactive=<filename> ]
7-
[--] [ <arg>... ]
8-
ptpython -h | --help
9-
10-
Options:
11-
--vi : Use Vi keybindings instead of Emacs bindings.
12-
--config-dir=<directory> : Pass config directory. By default '$XDG_CONFIG_HOME/ptpython'.
13-
-i, --interactive=<filename> : Start interactive shell after executing this file.
14-
"""
15-
from __future__ import absolute_import, unicode_literals, print_function
16-
17-
import appdirs
18-
import docopt
192
import os
20-
import six
213
import sys
224

5+
from .run_ptpython import create_parser, get_config_and_history_file
236

24-
def run(user_ns=None):
25-
a = docopt.docopt(__doc__)
26-
27-
vi_mode = bool(a['--vi'])
28-
29-
config_dir = appdirs.user_config_dir('ptpython', 'prompt_toolkit')
30-
data_dir = appdirs.user_data_dir('ptpython', 'prompt_toolkit')
317

32-
if a['--config-dir']:
33-
# Override config_dir.
34-
config_dir = os.path.expanduser(a['--config-dir'])
35-
else:
36-
# Warn about the legacy directory.
37-
legacy_dir = os.path.expanduser('~/.ptpython')
38-
if os.path.isdir(legacy_dir):
39-
print('{0} is deprecated, migrate your configuration to {1}'.format(legacy_dir, config_dir))
8+
def run(user_ns=None):
9+
a = create_parser().parse_args()
4010

41-
# Create directories.
42-
for d in (config_dir, data_dir):
43-
if not os.path.isdir(d) and not os.path.islink(d):
44-
os.mkdir(d)
11+
config_file, history_file = get_config_and_history_file(a)
4512

4613
# If IPython is not available, show message and exit here with error status
4714
# code.
4815
try:
4916
import IPython
5017
except ImportError:
51-
print('IPython not found. Please install IPython (pip install ipython).')
18+
print("IPython not found. Please install IPython (pip install ipython).")
5219
sys.exit(1)
5320
else:
5421
from ptpython.ipython import embed
5522
from ptpython.repl import run_config, enable_deprecation_warnings
5623

5724
# Add the current directory to `sys.path`.
58-
if sys.path[0] != '':
59-
sys.path.insert(0, '')
25+
if sys.path[0] != "":
26+
sys.path.insert(0, "")
6027

6128
# When a file has been given, run that, otherwise start the shell.
62-
if a['<arg>'] and not a['--interactive']:
63-
sys.argv = a['<arg>']
64-
path = a['<arg>'][0]
65-
with open(path, 'rb') as f:
66-
code = compile(f.read(), path, 'exec')
67-
six.exec_(code)
29+
if a.args and not a.interactive:
30+
sys.argv = a.args
31+
path = a.args[0]
32+
with open(path, "rb") as f:
33+
code = compile(f.read(), path, "exec")
34+
exec(code, {})
6835
else:
6936
enable_deprecation_warnings()
7037

@@ -76,37 +43,38 @@ def run(user_ns=None):
7643

7744
# Startup path
7845
startup_paths = []
79-
if 'PYTHONSTARTUP' in os.environ:
80-
startup_paths.append(os.environ['PYTHONSTARTUP'])
46+
if "PYTHONSTARTUP" in os.environ:
47+
startup_paths.append(os.environ["PYTHONSTARTUP"])
8148

8249
# --interactive
83-
if a['--interactive']:
84-
startup_paths.append(a['--interactive'])
85-
sys.argv = [a['--interactive']] + a['<arg>']
50+
if a.interactive:
51+
startup_paths.append(a.args[0])
52+
sys.argv = a.args
8653

8754
# exec scripts from startup paths
8855
for path in startup_paths:
8956
if os.path.exists(path):
90-
with open(path, 'rb') as f:
91-
code = compile(f.read(), path, 'exec')
92-
six.exec_(code, user_ns, user_ns)
57+
with open(path, "rb") as f:
58+
code = compile(f.read(), path, "exec")
59+
exec(code, user_ns, user_ns)
9360
else:
94-
print('File not found: {}\n\n'.format(path))
61+
print("File not found: {}\n\n".format(path))
9562
sys.exit(1)
9663

9764
# Apply config file
9865
def configure(repl):
99-
path = os.path.join(config_dir, 'config.py')
100-
if os.path.exists(path):
101-
run_config(repl, path)
66+
if os.path.exists(config_file):
67+
run_config(repl, config_file)
10268

10369
# Run interactive shell.
104-
embed(vi_mode=vi_mode,
105-
history_filename=os.path.join(data_dir, 'history'),
106-
configure=configure,
107-
user_ns=user_ns,
108-
title='IPython REPL (ptipython)')
70+
embed(
71+
vi_mode=a.vi,
72+
history_filename=history_file,
73+
configure=configure,
74+
user_ns=user_ns,
75+
title="IPython REPL (ptipython)",
76+
)
10977

11078

111-
if __name__ == '__main__':
79+
if __name__ == "__main__":
11280
run()

‎ptpython/entry_points/run_ptpython.py

+125-48
Original file line numberDiff line numberDiff line change
@@ -15,81 +15,158 @@
1515
Other environment variables:
1616
PYTHONSTARTUP: file executed on interactive startup (no default)
1717
"""
18-
from __future__ import absolute_import, unicode_literals, print_function
19-
20-
import appdirs
21-
import docopt
18+
import argparse
2219
import os
23-
import six
2420
import sys
21+
from typing import Tuple
2522

26-
from ptpython.repl import embed, enable_deprecation_warnings, run_config
27-
28-
29-
def run():
30-
a = docopt.docopt(__doc__)
31-
32-
vi_mode = bool(a['--vi'])
23+
import appdirs
24+
from prompt_toolkit.formatted_text import HTML
25+
from prompt_toolkit.shortcuts import print_formatted_text
3326

34-
config_dir = appdirs.user_config_dir('ptpython', 'prompt_toolkit')
35-
data_dir = appdirs.user_data_dir('ptpython', 'prompt_toolkit')
27+
from ptpython.repl import embed, enable_deprecation_warnings, run_config
3628

37-
if a['--config-dir']:
38-
# Override config_dir.
39-
config_dir = os.path.expanduser(a['--config-dir'])
40-
else:
41-
# Warn about the legacy directory.
42-
legacy_dir = os.path.expanduser('~/.ptpython')
43-
if os.path.isdir(legacy_dir):
44-
print('{0} is deprecated, migrate your configuration to {1}'.format(legacy_dir, config_dir))
29+
__all__ = ["create_parser", "get_config_and_history_file", "run"]
30+
31+
32+
class _Parser(argparse.ArgumentParser):
33+
def print_help(self):
34+
super().print_help()
35+
print("Other environment variables:")
36+
print("PYTHONSTARTUP: file executed on interactive startup (no default)")
37+
38+
39+
def create_parser() -> _Parser:
40+
parser = _Parser(description="ptpython: Interactive Python shell.")
41+
parser.add_argument("--vi", action="store_true", help="Enable Vi key bindings")
42+
parser.add_argument(
43+
"-i",
44+
"--interactive",
45+
action="store_true",
46+
help="Start interactive shell after executing this file.",
47+
)
48+
parser.add_argument(
49+
"--config-file", type=str, help="Location of configuration file."
50+
)
51+
parser.add_argument("--history-file", type=str, help="Location of history file.")
52+
parser.add_argument(
53+
"-V", "--version", action="store_true", help="Print version and exit."
54+
)
55+
parser.add_argument("args", nargs="*", help="Script and arguments")
56+
return parser
57+
58+
59+
def get_config_and_history_file(namespace: argparse.Namespace) -> Tuple[str, str]:
60+
"""
61+
Check which config/history files to use, ensure that the directories for
62+
these files exist, and return the config and history path.
63+
"""
64+
config_dir = appdirs.user_config_dir("ptpython", "prompt_toolkit")
65+
data_dir = appdirs.user_data_dir("ptpython", "prompt_toolkit")
4566

4667
# Create directories.
4768
for d in (config_dir, data_dir):
4869
if not os.path.isdir(d) and not os.path.islink(d):
4970
os.mkdir(d)
5071

72+
# Determine config file to be used.
73+
config_file = os.path.join(config_dir, "config.py")
74+
legacy_config_file = os.path.join(os.path.expanduser("~/.ptpython"), "config.py")
75+
76+
warnings = []
77+
78+
# Config file
79+
if namespace.config_file:
80+
# Override config_file.
81+
config_file = os.path.expanduser(namespace.config_file)
82+
83+
elif os.path.isfile(legacy_config_file):
84+
# Warn about the legacy configuration file.
85+
warnings.append(
86+
HTML(
87+
" <i>~/.ptpython/config.py</i> is deprecated, move your configuration to <i>%s</i>\n"
88+
)
89+
% config_file
90+
)
91+
config_file = legacy_config_file
92+
93+
# Determine history file to be used.
94+
history_file = os.path.join(data_dir, "history")
95+
legacy_history_file = os.path.join(os.path.expanduser("~/.ptpython"), "history")
96+
97+
if namespace.history_file:
98+
# Override history_file.
99+
history_file = os.path.expanduser(namespace.history_file)
100+
101+
elif os.path.isfile(legacy_history_file):
102+
# Warn about the legacy history file.
103+
warnings.append(
104+
HTML(
105+
" <i>~/.ptpython/history</i> is deprecated, move your history to <i>%s</i>\n"
106+
)
107+
% history_file
108+
)
109+
history_file = legacy_history_file
110+
111+
# Print warnings.
112+
if warnings:
113+
print_formatted_text(HTML("<u>Warning:</u>"))
114+
for w in warnings:
115+
print_formatted_text(w)
116+
117+
return config_file, history_file
118+
119+
120+
def run() -> None:
121+
a = create_parser().parse_args()
122+
123+
config_file, history_file = get_config_and_history_file(a)
124+
51125
# Startup path
52126
startup_paths = []
53-
if 'PYTHONSTARTUP' in os.environ:
54-
startup_paths.append(os.environ['PYTHONSTARTUP'])
127+
if "PYTHONSTARTUP" in os.environ:
128+
startup_paths.append(os.environ["PYTHONSTARTUP"])
55129

56130
# --interactive
57-
if a['--interactive']:
58-
startup_paths.append(a['--interactive'])
59-
sys.argv = [a['--interactive']] + a['<arg>']
131+
if a.interactive and a.args:
132+
startup_paths.append(a.args[0])
133+
sys.argv = a.args
60134

61135
# Add the current directory to `sys.path`.
62-
if sys.path[0] != '':
63-
sys.path.insert(0, '')
136+
if sys.path[0] != "":
137+
sys.path.insert(0, "")
64138

65139
# When a file has been given, run that, otherwise start the shell.
66-
if a['<arg>'] and not a['--interactive']:
67-
sys.argv = a['<arg>']
68-
path = a['<arg>'][0]
69-
with open(path, 'rb') as f:
70-
code = compile(f.read(), path, 'exec')
140+
if a.args and not a.interactive:
141+
sys.argv = a.args
142+
path = a.args[0]
143+
with open(path, "rb") as f:
144+
code = compile(f.read(), path, "exec")
71145
# NOTE: We have to pass an empty dictionary as namespace. Omitting
72146
# this argument causes imports to not be found. See issue #326.
73-
six.exec_(code, {})
147+
exec(code, {})
74148

75149
# Run interactive shell.
76150
else:
77151
enable_deprecation_warnings()
78152

79153
# Apply config file
80-
def configure(repl):
81-
path = os.path.join(config_dir, 'config.py')
82-
if os.path.exists(path):
83-
run_config(repl, path)
154+
def configure(repl) -> None:
155+
if os.path.exists(config_file):
156+
run_config(repl, config_file)
84157

85158
import __main__
86-
embed(vi_mode=vi_mode,
87-
history_filename=os.path.join(data_dir, 'history'),
88-
configure=configure,
89-
locals=__main__.__dict__,
90-
globals=__main__.__dict__,
91-
startup_paths=startup_paths,
92-
title='Python REPL (ptpython)')
93-
94-
if __name__ == '__main__':
159+
160+
embed(
161+
vi_mode=a.vi,
162+
history_filename=history_file,
163+
configure=configure,
164+
locals=__main__.__dict__,
165+
globals=__main__.__dict__,
166+
startup_paths=startup_paths,
167+
title="Python REPL (ptpython)",
168+
)
169+
170+
171+
if __name__ == "__main__":
95172
run()

‎ptpython/eventloop.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
import sys
1111
import time
1212

13-
__all__ = (
14-
'inputhook',
15-
)
13+
__all__ = ["inputhook"]
1614

1715

1816
def _inputhook_tk(inputhook_context):
@@ -22,7 +20,8 @@ def _inputhook_tk(inputhook_context):
2220
"""
2321
# Get the current TK application.
2422
import _tkinter # Keep this imports inline!
25-
from six.moves import tkinter
23+
import tkinter
24+
2625
root = tkinter._default_root
2726

2827
def wait_using_filehandler():
@@ -33,6 +32,7 @@ def wait_using_filehandler():
3332
# Add a handler that sets the stop flag when `prompt-toolkit` has input
3433
# to process.
3534
stop = [False]
35+
3636
def done(*a):
3737
stop[0] = True
3838

@@ -52,19 +52,19 @@ def wait_using_polling():
5252
"""
5353
while not inputhook_context.input_is_ready():
5454
while root.dooneevent(_tkinter.ALL_EVENTS | _tkinter.DONT_WAIT):
55-
pass
55+
pass
5656
# Sleep to make the CPU idle, but not too long, so that the UI
5757
# stays responsive.
58-
time.sleep(.01)
58+
time.sleep(0.01)
5959

6060
if root is not None:
61-
if hasattr(root, 'createfilehandler'):
61+
if hasattr(root, "createfilehandler"):
6262
wait_using_filehandler()
6363
else:
6464
wait_using_polling()
6565

6666

6767
def inputhook(inputhook_context):
6868
# Only call the real input hook when the 'Tkinter' library was loaded.
69-
if 'Tkinter' in sys.modules or 'tkinter' in sys.modules:
69+
if "Tkinter" in sys.modules or "tkinter" in sys.modules:
7070
_inputhook_tk(inputhook_context)

‎ptpython/filters.py

+11-13
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,36 @@
1-
from __future__ import unicode_literals
1+
from typing import TYPE_CHECKING
22

33
from prompt_toolkit.filters import Filter
44

5-
__all__ = (
6-
'HasSignature',
7-
'ShowSidebar',
8-
'ShowSignature',
9-
'ShowDocstring',
10-
)
5+
if TYPE_CHECKING:
6+
from .python_input import PythonInput
7+
8+
__all__ = ["HasSignature", "ShowSidebar", "ShowSignature", "ShowDocstring"]
119

1210

1311
class PythonInputFilter(Filter):
14-
def __init__(self, python_input):
12+
def __init__(self, python_input: "PythonInput") -> None:
1513
self.python_input = python_input
1614

17-
def __call__(self):
15+
def __call__(self) -> bool:
1816
raise NotImplementedError
1917

2018

2119
class HasSignature(PythonInputFilter):
22-
def __call__(self):
20+
def __call__(self) -> bool:
2321
return bool(self.python_input.signatures)
2422

2523

2624
class ShowSidebar(PythonInputFilter):
27-
def __call__(self):
25+
def __call__(self) -> bool:
2826
return self.python_input.show_sidebar
2927

3028

3129
class ShowSignature(PythonInputFilter):
32-
def __call__(self):
30+
def __call__(self) -> bool:
3331
return self.python_input.show_signature
3432

3533

3634
class ShowDocstring(PythonInputFilter):
37-
def __call__(self):
35+
def __call__(self) -> bool:
3836
return self.python_input.show_docstring

‎ptpython/history_browser.py

+202-152
Large diffs are not rendered by default.

‎ptpython/ipython.py

+99-77
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
offer.
99
1010
"""
11-
from __future__ import unicode_literals, print_function
12-
13-
from prompt_toolkit.completion import Completion, Completer
14-
from prompt_toolkit.completion import PathCompleter, WordCompleter
11+
from prompt_toolkit.completion import (
12+
Completer,
13+
Completion,
14+
PathCompleter,
15+
WordCompleter,
16+
)
1517
from prompt_toolkit.contrib.completers import SystemCompleter
1618
from prompt_toolkit.contrib.regular_languages.compiler import compile
1719
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
@@ -20,27 +22,25 @@
2022
from prompt_toolkit.formatted_text import PygmentsTokens
2123
from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer
2224
from prompt_toolkit.styles import Style
25+
from pygments.lexers import BashLexer, PythonLexer
2326

24-
from .python_input import PythonInput, PythonValidator, PythonCompleter
25-
from .style import default_ui_style
26-
27-
from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed
28-
from IPython.terminal.ipapp import load_default_config
2927
from IPython import utils as ipy_utils
3028
from IPython.core.inputsplitter import IPythonInputSplitter
31-
32-
from pygments.lexers import PythonLexer, BashLexer
29+
from IPython.terminal.embed import InteractiveShellEmbed as _InteractiveShellEmbed
30+
from IPython.terminal.ipapp import load_default_config
3331
from ptpython.prompt_style import PromptStyle
3432

35-
__all__ = (
36-
'embed',
37-
)
33+
from .python_input import PythonCompleter, PythonInput, PythonValidator
34+
from .style import default_ui_style
35+
36+
__all__ = ["embed"]
3837

3938

4039
class IPythonPrompt(PromptStyle):
4140
"""
4241
Style for IPython >5.0, use the prompt_toolkit tokens directly.
4342
"""
43+
4444
def __init__(self, prompts):
4545
self.prompts = prompts
4646

@@ -68,7 +68,8 @@ def create_ipython_grammar():
6868
"""
6969
Return compiled IPython grammar.
7070
"""
71-
return compile(r"""
71+
return compile(
72+
r"""
7273
\s*
7374
(
7475
(?P<percent>%)(
@@ -87,24 +88,37 @@ def create_ipython_grammar():
8788
(?![%!]) (?P<python>.+)
8889
)
8990
\s*
90-
""")
91+
"""
92+
)
9193

9294

93-
def create_completer(get_globals, get_locals, magics_manager, alias_manager,
94-
get_enable_dictionary_completion):
95+
def create_completer(
96+
get_globals,
97+
get_locals,
98+
magics_manager,
99+
alias_manager,
100+
get_enable_dictionary_completion,
101+
):
95102
g = create_ipython_grammar()
96103

97-
return GrammarCompleter(g, {
98-
'python': PythonCompleter(get_globals, get_locals, get_enable_dictionary_completion),
99-
'magic': MagicsCompleter(magics_manager),
100-
'alias_name': AliasCompleter(alias_manager),
101-
'pdb_arg': WordCompleter(['on', 'off'], ignore_case=True),
102-
'autocall_arg': WordCompleter(['0', '1', '2'], ignore_case=True),
103-
'py_filename': PathCompleter(only_directories=False, file_filter=lambda name: name.endswith('.py')),
104-
'filename': PathCompleter(only_directories=False),
105-
'directory': PathCompleter(only_directories=True),
106-
'system': SystemCompleter(),
107-
})
104+
return GrammarCompleter(
105+
g,
106+
{
107+
"python": PythonCompleter(
108+
get_globals, get_locals, get_enable_dictionary_completion
109+
),
110+
"magic": MagicsCompleter(magics_manager),
111+
"alias_name": AliasCompleter(alias_manager),
112+
"pdb_arg": WordCompleter(["on", "off"], ignore_case=True),
113+
"autocall_arg": WordCompleter(["0", "1", "2"], ignore_case=True),
114+
"py_filename": PathCompleter(
115+
only_directories=False, file_filter=lambda name: name.endswith(".py")
116+
),
117+
"filename": PathCompleter(only_directories=False),
118+
"directory": PathCompleter(only_directories=True),
119+
"system": SystemCompleter(),
120+
},
121+
)
108122

109123

110124
def create_lexer():
@@ -113,12 +127,13 @@ def create_lexer():
113127
return GrammarLexer(
114128
g,
115129
lexers={
116-
'percent': SimpleLexer('class:pygments.operator'),
117-
'magic': SimpleLexer('class:pygments.keyword'),
118-
'filename': SimpleLexer('class:pygments.name'),
119-
'python': PygmentsLexer(PythonLexer),
120-
'system': PygmentsLexer(BashLexer),
121-
})
130+
"percent": SimpleLexer("class:pygments.operator"),
131+
"magic": SimpleLexer("class:pygments.keyword"),
132+
"filename": SimpleLexer("class:pygments.name"),
133+
"python": PygmentsLexer(PythonLexer),
134+
"system": PygmentsLexer(BashLexer),
135+
},
136+
)
122137

123138

124139
class MagicsCompleter(Completer):
@@ -128,9 +143,9 @@ def __init__(self, magics_manager):
128143
def get_completions(self, document, complete_event):
129144
text = document.text_before_cursor.lstrip()
130145

131-
for m in sorted(self.magics_manager.magics['line']):
146+
for m in sorted(self.magics_manager.magics["line"]):
132147
if m.startswith(text):
133-
yield Completion('%s' % m, -len(text))
148+
yield Completion("%s" % m, -len(text))
134149

135150

136151
class AliasCompleter(Completer):
@@ -139,48 +154,50 @@ def __init__(self, alias_manager):
139154

140155
def get_completions(self, document, complete_event):
141156
text = document.text_before_cursor.lstrip()
142-
#aliases = [a for a, _ in self.alias_manager.aliases]
157+
# aliases = [a for a, _ in self.alias_manager.aliases]
143158
aliases = self.alias_manager.aliases
144159

145160
for a, cmd in sorted(aliases, key=lambda a: a[0]):
146161
if a.startswith(text):
147-
yield Completion('%s' % a, -len(text),
148-
display_meta=cmd)
162+
yield Completion("%s" % a, -len(text), display_meta=cmd)
149163

150164

151165
class IPythonInput(PythonInput):
152166
"""
153167
Override our `PythonCommandLineInterface` to add IPython specific stuff.
154168
"""
169+
155170
def __init__(self, ipython_shell, *a, **kw):
156-
kw['_completer'] = create_completer(kw['get_globals'], kw['get_globals'],
157-
ipython_shell.magics_manager,
158-
ipython_shell.alias_manager,
159-
lambda: self.enable_dictionary_completion)
160-
kw['_lexer'] = create_lexer()
161-
kw['_validator'] = IPythonValidator(
162-
get_compiler_flags=self.get_compiler_flags)
163-
164-
super(IPythonInput, self).__init__(*a, **kw)
171+
kw["_completer"] = create_completer(
172+
kw["get_globals"],
173+
kw["get_globals"],
174+
ipython_shell.magics_manager,
175+
ipython_shell.alias_manager,
176+
lambda: self.enable_dictionary_completion,
177+
)
178+
kw["_lexer"] = create_lexer()
179+
kw["_validator"] = IPythonValidator(get_compiler_flags=self.get_compiler_flags)
180+
181+
super().__init__(*a, **kw)
165182
self.ipython_shell = ipython_shell
166183

167-
self.all_prompt_styles['ipython'] = IPythonPrompt(ipython_shell.prompts)
168-
self.prompt_style = 'ipython'
184+
self.all_prompt_styles["ipython"] = IPythonPrompt(ipython_shell.prompts)
185+
self.prompt_style = "ipython"
169186

170187
# UI style for IPython. Add tokens that are used by IPython>5.0
171188
style_dict = {}
172189
style_dict.update(default_ui_style)
173-
style_dict.update({
174-
'pygments.prompt': '#009900',
175-
'pygments.prompt-num': '#00ff00 bold',
176-
'pygments.out-prompt': '#990000',
177-
'pygments.out-prompt-num': '#ff0000 bold',
178-
})
190+
style_dict.update(
191+
{
192+
"pygments.prompt": "#009900",
193+
"pygments.prompt-num": "#00ff00 bold",
194+
"pygments.out-prompt": "#990000",
195+
"pygments.out-prompt-num": "#ff0000 bold",
196+
}
197+
)
179198

180-
self.ui_styles = {
181-
'default': Style.from_dict(style_dict),
182-
}
183-
self.use_ui_colorscheme('default')
199+
self.ui_styles = {"default": Style.from_dict(style_dict)}
200+
self.use_ui_colorscheme("default")
184201

185202

186203
class InteractiveShellEmbed(_InteractiveShellEmbed):
@@ -190,31 +207,34 @@ class InteractiveShellEmbed(_InteractiveShellEmbed):
190207
191208
:param configure: Callable for configuring the repl.
192209
"""
210+
193211
def __init__(self, *a, **kw):
194-
vi_mode = kw.pop('vi_mode', False)
195-
history_filename = kw.pop('history_filename', None)
196-
configure = kw.pop('configure', None)
197-
title = kw.pop('title', None)
212+
vi_mode = kw.pop("vi_mode", False)
213+
history_filename = kw.pop("history_filename", None)
214+
configure = kw.pop("configure", None)
215+
title = kw.pop("title", None)
198216

199217
# Don't ask IPython to confirm for exit. We have our own exit prompt.
200218
self.confirm_exit = False
201219

202-
super(InteractiveShellEmbed, self).__init__(*a, **kw)
220+
super().__init__(*a, **kw)
203221

204222
def get_globals():
205223
return self.user_ns
206224

207225
python_input = IPythonInput(
208226
self,
209-
get_globals=get_globals, vi_mode=vi_mode,
210-
history_filename=history_filename)
227+
get_globals=get_globals,
228+
vi_mode=vi_mode,
229+
history_filename=history_filename,
230+
)
211231

212232
if title:
213233
python_input.terminal_title = title
214234

215235
if configure:
216236
configure(python_input)
217-
python_input.prompt_style = 'ipython' # Don't take from config.
237+
python_input.prompt_style = "ipython" # Don't take from config.
218238

219239
self.python_input = python_input
220240

@@ -223,7 +243,7 @@ def prompt_for_code(self):
223243
return self.python_input.app.run()
224244
except KeyboardInterrupt:
225245
self.python_input.default_buffer.document = Document()
226-
return ''
246+
return ""
227247

228248

229249
def initialize_extensions(shell, extensions):
@@ -240,22 +260,24 @@ def initialize_extensions(shell, extensions):
240260
shell.extension_manager.load_extension(ext)
241261
except:
242262
ipy_utils.warn.warn(
243-
"Error in loading extension: %s" % ext +
244-
"\nCheck your config files in %s" % ipy_utils.path.get_ipython_dir())
263+
"Error in loading extension: %s" % ext
264+
+ "\nCheck your config files in %s"
265+
% ipy_utils.path.get_ipython_dir()
266+
)
245267
shell.showtraceback()
246268

247269

248270
def embed(**kwargs):
249271
"""
250272
Copied from `IPython/terminal/embed.py`, but using our `InteractiveShellEmbed` instead.
251273
"""
252-
config = kwargs.get('config')
253-
header = kwargs.pop('header', u'')
254-
compile_flags = kwargs.pop('compile_flags', None)
274+
config = kwargs.get("config")
275+
header = kwargs.pop("header", "")
276+
compile_flags = kwargs.pop("compile_flags", None)
255277
if config is None:
256278
config = load_default_config()
257279
config.InteractiveShellEmbed = config.TerminalInteractiveShell
258-
kwargs['config'] = config
280+
kwargs["config"] = config
259281
shell = InteractiveShellEmbed.instance(**kwargs)
260-
initialize_extensions(shell, config['InteractiveShellApp']['extensions'])
282+
initialize_extensions(shell, config["InteractiveShellApp"]["extensions"])
261283
shell(header=header, stack_depth=2, compile_flags=compile_flags)

‎ptpython/key_bindings.py

+93-67
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
from __future__ import unicode_literals
2-
1+
from prompt_toolkit.application import get_app
32
from prompt_toolkit.document import Document
43
from prompt_toolkit.enums import DEFAULT_BUFFER
5-
from prompt_toolkit.filters import has_selection, has_focus, Condition, vi_insert_mode, emacs_insert_mode, emacs_mode
4+
from prompt_toolkit.filters import (
5+
Condition,
6+
emacs_insert_mode,
7+
emacs_mode,
8+
has_focus,
9+
has_selection,
10+
vi_insert_mode,
11+
)
612
from prompt_toolkit.key_binding import KeyBindings
713
from prompt_toolkit.keys import Keys
8-
from prompt_toolkit.application import get_app
14+
915
from .utils import document_is_multiline_python
1016

11-
__all__ = (
12-
'load_python_bindings',
13-
'load_sidebar_bindings',
14-
'load_confirm_exit_bindings',
15-
)
17+
__all__ = [
18+
"load_python_bindings",
19+
"load_sidebar_bindings",
20+
"load_confirm_exit_bindings",
21+
]
1622

1723

1824
@Condition
@@ -40,22 +46,22 @@ def load_python_bindings(python_input):
4046
sidebar_visible = Condition(lambda: python_input.show_sidebar)
4147
handle = bindings.add
4248

43-
@handle('c-l')
49+
@handle("c-l")
4450
def _(event):
4551
"""
4652
Clear whole screen and render again -- also when the sidebar is visible.
4753
"""
4854
event.app.renderer.clear()
4955

50-
@handle('c-z')
56+
@handle("c-z")
5157
def _(event):
5258
"""
5359
Suspend.
5460
"""
5561
if python_input.enable_system_bindings:
5662
event.app.suspend_to_background()
5763

58-
@handle('f2')
64+
@handle("f2")
5965
def _(event):
6066
"""
6167
Show/hide sidebar.
@@ -66,42 +72,49 @@ def _(event):
6672
else:
6773
event.app.layout.focus_last()
6874

69-
@handle('f3')
75+
@handle("f3")
7076
def _(event):
7177
"""
7278
Select from the history.
7379
"""
7480
python_input.enter_history()
7581

76-
@handle('f4')
82+
@handle("f4")
7783
def _(event):
7884
"""
7985
Toggle between Vi and Emacs mode.
8086
"""
8187
python_input.vi_mode = not python_input.vi_mode
8288

83-
@handle('f6')
89+
@handle("f6")
8490
def _(event):
8591
"""
8692
Enable/Disable paste mode.
8793
"""
8894
python_input.paste_mode = not python_input.paste_mode
8995

90-
@handle('tab', filter= ~sidebar_visible & ~has_selection & tab_should_insert_whitespace)
96+
@handle(
97+
"tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace
98+
)
9199
def _(event):
92100
"""
93101
When tab should insert whitespace, do that instead of completion.
94102
"""
95-
event.app.current_buffer.insert_text(' ')
103+
event.app.current_buffer.insert_text(" ")
96104

97105
@Condition
98106
def is_multiline():
99107
return document_is_multiline_python(python_input.default_buffer.document)
100108

101-
@handle('enter', filter= ~sidebar_visible & ~has_selection &
102-
(vi_insert_mode | emacs_insert_mode) &
103-
has_focus(DEFAULT_BUFFER) & ~is_multiline)
104-
@handle(Keys.Escape, Keys.Enter, filter= ~sidebar_visible & emacs_mode)
109+
@handle(
110+
"enter",
111+
filter=~sidebar_visible
112+
& ~has_selection
113+
& (vi_insert_mode | emacs_insert_mode)
114+
& has_focus(DEFAULT_BUFFER)
115+
& ~is_multiline,
116+
)
117+
@handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode)
105118
def _(event):
106119
"""
107120
Accept input (for single line input).
@@ -112,14 +125,19 @@ def _(event):
112125
# When the cursor is at the end, and we have an empty line:
113126
# drop the empty lines, but return the value.
114127
b.document = Document(
115-
text=b.text.rstrip(),
116-
cursor_position=len(b.text.rstrip()))
128+
text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
129+
)
117130

118131
b.validate_and_handle()
119132

120-
@handle('enter', filter= ~sidebar_visible & ~has_selection &
121-
(vi_insert_mode | emacs_insert_mode) &
122-
has_focus(DEFAULT_BUFFER) & is_multiline)
133+
@handle(
134+
"enter",
135+
filter=~sidebar_visible
136+
& ~has_selection
137+
& (vi_insert_mode | emacs_insert_mode)
138+
& has_focus(DEFAULT_BUFFER)
139+
& is_multiline,
140+
)
123141
def _(event):
124142
"""
125143
Behaviour of the Enter key.
@@ -134,30 +152,36 @@ def at_the_end(b):
134152
""" we consider the cursor at the end when there is no text after
135153
the cursor, or only whitespace. """
136154
text = b.document.text_after_cursor
137-
return text == '' or (text.isspace() and not '\n' in text)
155+
return text == "" or (text.isspace() and not "\n" in text)
138156

139157
if python_input.paste_mode:
140158
# In paste mode, always insert text.
141-
b.insert_text('\n')
159+
b.insert_text("\n")
142160

143-
elif at_the_end(b) and b.document.text.replace(' ', '').endswith(
144-
'\n' * (empty_lines_required - 1)):
161+
elif at_the_end(b) and b.document.text.replace(" ", "").endswith(
162+
"\n" * (empty_lines_required - 1)
163+
):
145164
# When the cursor is at the end, and we have an empty line:
146165
# drop the empty lines, but return the value.
147166
if b.validate():
148167
b.document = Document(
149-
text=b.text.rstrip(),
150-
cursor_position=len(b.text.rstrip()))
168+
text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
169+
)
151170

152171
b.validate_and_handle()
153172
else:
154173
auto_newline(b)
155174

156-
@handle('c-d', filter=~sidebar_visible &
157-
has_focus(python_input.default_buffer) &
158-
Condition(lambda:
159-
# The current buffer is empty.
160-
not get_app().current_buffer.text))
175+
@handle(
176+
"c-d",
177+
filter=~sidebar_visible
178+
& has_focus(python_input.default_buffer)
179+
& Condition(
180+
lambda:
181+
# The current buffer is empty.
182+
not get_app().current_buffer.text
183+
),
184+
)
161185
def _(event):
162186
"""
163187
Override Control-D exit, to ask for confirmation.
@@ -167,10 +191,10 @@ def _(event):
167191
else:
168192
event.app.exit(exception=EOFError)
169193

170-
@handle('c-c', filter=has_focus(python_input.default_buffer))
194+
@handle("c-c", filter=has_focus(python_input.default_buffer))
171195
def _(event):
172196
" Abort when Control-C has been pressed. "
173-
event.app.exit(exception=KeyboardInterrupt, style='class:aborting')
197+
event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
174198

175199
return bindings
176200

@@ -184,42 +208,44 @@ def load_sidebar_bindings(python_input):
184208
handle = bindings.add
185209
sidebar_visible = Condition(lambda: python_input.show_sidebar)
186210

187-
@handle('up', filter=sidebar_visible)
188-
@handle('c-p', filter=sidebar_visible)
189-
@handle('k', filter=sidebar_visible)
211+
@handle("up", filter=sidebar_visible)
212+
@handle("c-p", filter=sidebar_visible)
213+
@handle("k", filter=sidebar_visible)
190214
def _(event):
191215
" Go to previous option. "
192216
python_input.selected_option_index = (
193-
(python_input.selected_option_index - 1) % python_input.option_count)
217+
python_input.selected_option_index - 1
218+
) % python_input.option_count
194219

195-
@handle('down', filter=sidebar_visible)
196-
@handle('c-n', filter=sidebar_visible)
197-
@handle('j', filter=sidebar_visible)
220+
@handle("down", filter=sidebar_visible)
221+
@handle("c-n", filter=sidebar_visible)
222+
@handle("j", filter=sidebar_visible)
198223
def _(event):
199224
" Go to next option. "
200225
python_input.selected_option_index = (
201-
(python_input.selected_option_index + 1) % python_input.option_count)
226+
python_input.selected_option_index + 1
227+
) % python_input.option_count
202228

203-
@handle('right', filter=sidebar_visible)
204-
@handle('l', filter=sidebar_visible)
205-
@handle(' ', filter=sidebar_visible)
229+
@handle("right", filter=sidebar_visible)
230+
@handle("l", filter=sidebar_visible)
231+
@handle(" ", filter=sidebar_visible)
206232
def _(event):
207233
" Select next value for current option. "
208234
option = python_input.selected_option
209235
option.activate_next()
210236

211-
@handle('left', filter=sidebar_visible)
212-
@handle('h', filter=sidebar_visible)
237+
@handle("left", filter=sidebar_visible)
238+
@handle("h", filter=sidebar_visible)
213239
def _(event):
214240
" Select previous value for current option. "
215241
option = python_input.selected_option
216242
option.activate_previous()
217243

218-
@handle('c-c', filter=sidebar_visible)
219-
@handle('c-d', filter=sidebar_visible)
220-
@handle('c-d', filter=sidebar_visible)
221-
@handle('enter', filter=sidebar_visible)
222-
@handle('escape', filter=sidebar_visible)
244+
@handle("c-c", filter=sidebar_visible)
245+
@handle("c-d", filter=sidebar_visible)
246+
@handle("c-d", filter=sidebar_visible)
247+
@handle("enter", filter=sidebar_visible)
248+
@handle("escape", filter=sidebar_visible)
223249
def _(event):
224250
" Hide sidebar. "
225251
python_input.show_sidebar = False
@@ -237,15 +263,15 @@ def load_confirm_exit_bindings(python_input):
237263
handle = bindings.add
238264
confirmation_visible = Condition(lambda: python_input.show_exit_confirmation)
239265

240-
@handle('y', filter=confirmation_visible)
241-
@handle('Y', filter=confirmation_visible)
242-
@handle('enter', filter=confirmation_visible)
243-
@handle('c-d', filter=confirmation_visible)
266+
@handle("y", filter=confirmation_visible)
267+
@handle("Y", filter=confirmation_visible)
268+
@handle("enter", filter=confirmation_visible)
269+
@handle("c-d", filter=confirmation_visible)
244270
def _(event):
245271
"""
246272
Really quit.
247273
"""
248-
event.app.exit(exception=EOFError, style='class:exiting')
274+
event.app.exit(exception=EOFError, style="class:exiting")
249275

250276
@handle(Keys.Any, filter=confirmation_visible)
251277
def _(event):
@@ -265,14 +291,14 @@ def auto_newline(buffer):
265291

266292
if buffer.document.current_line_after_cursor:
267293
# When we are in the middle of a line. Always insert a newline.
268-
insert_text('\n')
294+
insert_text("\n")
269295
else:
270296
# Go to new line, but also add indentation.
271297
current_line = buffer.document.current_line_before_cursor.rstrip()
272-
insert_text('\n')
298+
insert_text("\n")
273299

274300
# Unident if the last line ends with 'pass', remove four spaces.
275-
unindent = current_line.rstrip().endswith(' pass')
301+
unindent = current_line.rstrip().endswith(" pass")
276302

277303
# Copy whitespace from current line
278304
current_line2 = current_line[4:] if unindent else current_line
@@ -284,6 +310,6 @@ def auto_newline(buffer):
284310
break
285311

286312
# If the last line ends with a colon, add four extra spaces.
287-
if current_line[-1:] == ':':
313+
if current_line[-1:] == ":":
288314
for x in range(4):
289-
insert_text(' ')
315+
insert_text(" ")

‎ptpython/layout.py

+415-283
Large diffs are not rendered by default.

‎ptpython/prompt_style.py

+31-30
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
from __future__ import unicode_literals
21
from abc import ABCMeta, abstractmethod
3-
from six import with_metaclass
2+
from typing import TYPE_CHECKING
43

5-
__all__ = (
6-
'PromptStyle',
7-
'IPythonPrompt',
8-
'ClassicPrompt',
9-
)
4+
from prompt_toolkit.formatted_text import StyleAndTextTuples
105

6+
if TYPE_CHECKING:
7+
from .python_input import PythonInput
118

12-
class PromptStyle(with_metaclass(ABCMeta, object)):
9+
__all__ = ["PromptStyle", "IPythonPrompt", "ClassicPrompt"]
10+
11+
12+
class PromptStyle(metaclass=ABCMeta):
1313
"""
1414
Base class for all prompts.
1515
"""
16+
1617
@abstractmethod
17-
def in_prompt(self):
18+
def in_prompt(self) -> StyleAndTextTuples:
1819
" Return the input tokens. "
1920
return []
2021

2122
@abstractmethod
22-
def in2_prompt(self, width):
23+
def in2_prompt(self, width: int) -> StyleAndTextTuples:
2324
"""
2425
Tokens for every following input line.
2526
@@ -29,7 +30,7 @@ def in2_prompt(self, width):
2930
return []
3031

3132
@abstractmethod
32-
def out_prompt(self):
33+
def out_prompt(self) -> StyleAndTextTuples:
3334
" Return the output tokens. "
3435
return []
3536

@@ -38,39 +39,39 @@ class IPythonPrompt(PromptStyle):
3839
"""
3940
A prompt resembling the IPython prompt.
4041
"""
41-
def __init__(self, python_input):
42+
43+
def __init__(self, python_input: "PythonInput") -> None:
4244
self.python_input = python_input
4345

44-
def in_prompt(self):
46+
def in_prompt(self) -> StyleAndTextTuples:
4547
return [
46-
('class:in', 'In ['),
47-
('class:in.number', '%s' % self.python_input.current_statement_index),
48-
('class:in', ']: '),
48+
("class:in", "In ["),
49+
("class:in.number", "%s" % self.python_input.current_statement_index),
50+
("class:in", "]: "),
4951
]
5052

51-
def in2_prompt(self, width):
52-
return [
53-
('class:in', '...: '.rjust(width)),
54-
]
53+
def in2_prompt(self, width: int) -> StyleAndTextTuples:
54+
return [("class:in", "...: ".rjust(width))]
5555

56-
def out_prompt(self):
56+
def out_prompt(self) -> StyleAndTextTuples:
5757
return [
58-
('class:out', 'Out['),
59-
('class:out.number', '%s' % self.python_input.current_statement_index),
60-
('class:out', ']:'),
61-
('', ' '),
58+
("class:out", "Out["),
59+
("class:out.number", "%s" % self.python_input.current_statement_index),
60+
("class:out", "]:"),
61+
("", " "),
6262
]
6363

6464

6565
class ClassicPrompt(PromptStyle):
6666
"""
6767
The classic Python prompt.
6868
"""
69-
def in_prompt(self):
70-
return [('class:prompt', '>>> ')]
7169

72-
def in2_prompt(self, width):
73-
return [('class:prompt.dots', '...')]
70+
def in_prompt(self) -> StyleAndTextTuples:
71+
return [("class:prompt", ">>> ")]
72+
73+
def in2_prompt(self, width: int) -> StyleAndTextTuples:
74+
return [("class:prompt.dots", "...")]
7475

75-
def out_prompt(self):
76+
def out_prompt(self) -> StyleAndTextTuples:
7677
return []

‎ptpython/python_input.py

+531-368
Large diffs are not rendered by default.

‎ptpython/repl.py

+148-123
Large diffs are not rendered by default.

‎ptpython/style.py

+94-117
Original file line numberDiff line numberDiff line change
@@ -1,174 +1,151 @@
1-
from __future__ import unicode_literals
1+
from typing import Dict
22

3-
from prompt_toolkit.styles import Style, merge_styles
3+
from prompt_toolkit.styles import BaseStyle, Style, merge_styles
44
from prompt_toolkit.styles.pygments import style_from_pygments_cls
5-
from prompt_toolkit.utils import is_windows, is_conemu_ansi, is_windows_vt100_supported
6-
from pygments.styles import get_style_by_name, get_all_styles
5+
from prompt_toolkit.utils import is_conemu_ansi, is_windows, is_windows_vt100_supported
6+
from pygments.styles import get_all_styles, get_style_by_name
77

8-
__all__ = (
9-
'get_all_code_styles',
10-
'get_all_ui_styles',
11-
'generate_style',
12-
)
8+
__all__ = ["get_all_code_styles", "get_all_ui_styles", "generate_style"]
139

1410

15-
def get_all_code_styles():
11+
def get_all_code_styles() -> Dict[str, BaseStyle]:
1612
"""
1713
Return a mapping from style names to their classes.
1814
"""
19-
result = dict((name, style_from_pygments_cls(get_style_by_name(name))) for name in get_all_styles())
20-
result['win32'] = Style.from_dict(win32_code_style)
15+
result: Dict[str, BaseStyle] = {
16+
name: style_from_pygments_cls(get_style_by_name(name))
17+
for name in get_all_styles()
18+
}
19+
result["win32"] = Style.from_dict(win32_code_style)
2120
return result
2221

2322

24-
def get_all_ui_styles():
23+
def get_all_ui_styles() -> Dict[str, BaseStyle]:
2524
"""
2625
Return a dict mapping {ui_style_name -> style_dict}.
2726
"""
2827
return {
29-
'default': Style.from_dict(default_ui_style),
30-
'blue': Style.from_dict(blue_ui_style),
28+
"default": Style.from_dict(default_ui_style),
29+
"blue": Style.from_dict(blue_ui_style),
3130
}
3231

3332

34-
def generate_style(python_style, ui_style):
33+
def generate_style(python_style: BaseStyle, ui_style: BaseStyle) -> BaseStyle:
3534
"""
3635
Generate Pygments Style class from two dictionaries
3736
containing style rules.
3837
"""
39-
return merge_styles([
40-
python_style,
41-
ui_style
42-
])
38+
return merge_styles([python_style, ui_style])
4339

4440

4541
# Code style for Windows consoles. They support only 16 colors,
4642
# so we choose a combination that displays nicely.
4743
win32_code_style = {
48-
'pygments.comment': "#00ff00",
49-
'pygments.keyword': '#44ff44',
50-
'pygments.number': '',
51-
'pygments.operator': '',
52-
'pygments.string': '#ff44ff',
53-
54-
'pygments.name': '',
55-
'pygments.name.decorator': '#ff4444',
56-
'pygments.name.class': '#ff4444',
57-
'pygments.name.function': '#ff4444',
58-
'pygments.name.builtin': '#ff4444',
59-
60-
'pygments.name.attribute': '',
61-
'pygments.name.constant': '',
62-
'pygments.name.entity': '',
63-
'pygments.name.exception': '',
64-
'pygments.name.label': '',
65-
'pygments.name.namespace': '',
66-
'pygments.name.tag': '',
67-
'pygments.name.variable': '',
44+
"pygments.comment": "#00ff00",
45+
"pygments.keyword": "#44ff44",
46+
"pygments.number": "",
47+
"pygments.operator": "",
48+
"pygments.string": "#ff44ff",
49+
"pygments.name": "",
50+
"pygments.name.decorator": "#ff4444",
51+
"pygments.name.class": "#ff4444",
52+
"pygments.name.function": "#ff4444",
53+
"pygments.name.builtin": "#ff4444",
54+
"pygments.name.attribute": "",
55+
"pygments.name.constant": "",
56+
"pygments.name.entity": "",
57+
"pygments.name.exception": "",
58+
"pygments.name.label": "",
59+
"pygments.name.namespace": "",
60+
"pygments.name.tag": "",
61+
"pygments.name.variable": "",
6862
}
6963

7064

7165
default_ui_style = {
72-
'control-character': 'ansiblue',
73-
66+
"control-character": "ansiblue",
7467
# Classic prompt.
75-
'prompt': 'bold',
76-
'prompt.dots': 'noinherit',
77-
68+
"prompt": "bold",
69+
"prompt.dots": "noinherit",
7870
# (IPython <5.0) Prompt: "In [1]:"
79-
'in': 'bold #008800',
80-
'in.number': '',
81-
71+
"in": "bold #008800",
72+
"in.number": "",
8273
# Return value.
83-
'out': '#ff0000',
84-
'out.number': '#ff0000',
85-
74+
"out": "#ff0000",
75+
"out.number": "#ff0000",
8676
# Completions.
87-
'completion.builtin': '',
88-
'completion.keyword': 'fg:#008800',
89-
90-
'completion.keyword fuzzymatch.inside': 'fg:#008800',
91-
'completion.keyword fuzzymatch.outside': 'fg:#44aa44',
92-
77+
"completion.builtin": "",
78+
"completion.keyword": "fg:#008800",
79+
"completion.keyword fuzzymatch.inside": "fg:#008800",
80+
"completion.keyword fuzzymatch.outside": "fg:#44aa44",
9381
# Separator between windows. (Used above docstring.)
94-
'separator': '#bbbbbb',
95-
82+
"separator": "#bbbbbb",
9683
# System toolbar
97-
'system-toolbar': '#22aaaa noinherit',
98-
84+
"system-toolbar": "#22aaaa noinherit",
9985
# "arg" toolbar.
100-
'arg-toolbar': '#22aaaa noinherit',
101-
'arg-toolbar.text': 'noinherit',
102-
86+
"arg-toolbar": "#22aaaa noinherit",
87+
"arg-toolbar.text": "noinherit",
10388
# Signature toolbar.
104-
'signature-toolbar': 'bg:#44bbbb #000000',
105-
'signature-toolbar.currentname': 'bg:#008888 #ffffff bold',
106-
'signature-toolbar.operator': '#000000 bold',
107-
108-
'docstring': '#888888',
109-
89+
"signature-toolbar": "bg:#44bbbb #000000",
90+
"signature-toolbar.currentname": "bg:#008888 #ffffff bold",
91+
"signature-toolbar.operator": "#000000 bold",
92+
"docstring": "#888888",
11093
# Validation toolbar.
111-
'validation-toolbar': 'bg:#440000 #aaaaaa',
112-
94+
"validation-toolbar": "bg:#440000 #aaaaaa",
11395
# Status toolbar.
114-
'status-toolbar': 'bg:#222222 #aaaaaa',
115-
'status-toolbar.title': 'underline',
116-
'status-toolbar.inputmode': 'bg:#222222 #ffffaa',
117-
'status-toolbar.key': 'bg:#000000 #888888',
118-
'status-toolbar.pastemodeon': 'bg:#aa4444 #ffffff',
119-
'status-toolbar.pythonversion': 'bg:#222222 #ffffff bold',
120-
'status-toolbar paste-mode-on': 'bg:#aa4444 #ffffff',
121-
'record': 'bg:#884444 white',
122-
'status-toolbar.input-mode': '#ffff44',
123-
96+
"status-toolbar": "bg:#222222 #aaaaaa",
97+
"status-toolbar.title": "underline",
98+
"status-toolbar.inputmode": "bg:#222222 #ffffaa",
99+
"status-toolbar.key": "bg:#000000 #888888",
100+
"status-toolbar.pastemodeon": "bg:#aa4444 #ffffff",
101+
"status-toolbar.pythonversion": "bg:#222222 #ffffff bold",
102+
"status-toolbar paste-mode-on": "bg:#aa4444 #ffffff",
103+
"record": "bg:#884444 white",
104+
"status-toolbar.input-mode": "#ffff44",
124105
# The options sidebar.
125-
'sidebar': 'bg:#bbbbbb #000000',
126-
'sidebar.title': 'bg:#668866 #ffffff',
127-
'sidebar.label': 'bg:#bbbbbb #222222',
128-
'sidebar.status': 'bg:#dddddd #000011',
129-
'sidebar.label selected': 'bg:#222222 #eeeeee',
130-
'sidebar.status selected': 'bg:#444444 #ffffff bold',
131-
132-
'sidebar.separator': 'underline',
133-
'sidebar.key': 'bg:#bbddbb #000000 bold',
134-
'sidebar.key.description': 'bg:#bbbbbb #000000',
135-
'sidebar.helptext': 'bg:#fdf6e3 #000011',
136-
137-
# # Styling for the history layout.
138-
# history.line: '',
139-
# history.line.selected: 'bg:#008800 #000000',
140-
# history.line.current: 'bg:#ffffff #000000',
141-
# history.line.selected.current: 'bg:#88ff88 #000000',
142-
# history.existinginput: '#888888',
143-
106+
"sidebar": "bg:#bbbbbb #000000",
107+
"sidebar.title": "bg:#668866 #ffffff",
108+
"sidebar.label": "bg:#bbbbbb #222222",
109+
"sidebar.status": "bg:#dddddd #000011",
110+
"sidebar.label selected": "bg:#222222 #eeeeee",
111+
"sidebar.status selected": "bg:#444444 #ffffff bold",
112+
"sidebar.separator": "underline",
113+
"sidebar.key": "bg:#bbddbb #000000 bold",
114+
"sidebar.key.description": "bg:#bbbbbb #000000",
115+
"sidebar.helptext": "bg:#fdf6e3 #000011",
116+
# # Styling for the history layout.
117+
# history.line: '',
118+
# history.line.selected: 'bg:#008800 #000000',
119+
# history.line.current: 'bg:#ffffff #000000',
120+
# history.line.selected.current: 'bg:#88ff88 #000000',
121+
# history.existinginput: '#888888',
144122
# Help Window.
145-
'window-border': '#aaaaaa',
146-
'window-title': 'bg:#bbbbbb #000000',
147-
123+
"window-border": "#aaaaaa",
124+
"window-title": "bg:#bbbbbb #000000",
148125
# Meta-enter message.
149-
'accept-message': 'bg:#ffff88 #444444',
150-
126+
"accept-message": "bg:#ffff88 #444444",
151127
# Exit confirmation.
152-
'exit-confirmation': 'bg:#884444 #ffffff',
128+
"exit-confirmation": "bg:#884444 #ffffff",
153129
}
154130

155131

156132
# Some changes to get a bit more contrast on Windows consoles.
157133
# (They only support 16 colors.)
158134
if is_windows() and not is_conemu_ansi() and not is_windows_vt100_supported():
159-
default_ui_style.update({
160-
'sidebar.title': 'bg:#00ff00 #ffffff',
161-
'exitconfirmation': 'bg:#ff4444 #ffffff',
162-
'toolbar.validation': 'bg:#ff4444 #ffffff',
163-
164-
'menu.completions.completion': 'bg:#ffffff #000000',
165-
'menu.completions.completion.current': 'bg:#aaaaaa #000000',
166-
})
135+
default_ui_style.update(
136+
{
137+
"sidebar.title": "bg:#00ff00 #ffffff",
138+
"exitconfirmation": "bg:#ff4444 #ffffff",
139+
"toolbar.validation": "bg:#ff4444 #ffffff",
140+
"menu.completions.completion": "bg:#ffffff #000000",
141+
"menu.completions.completion.current": "bg:#aaaaaa #000000",
142+
}
143+
)
167144

168145

169146
blue_ui_style = {}
170147
blue_ui_style.update(default_ui_style)
171-
#blue_ui_style.update({
148+
# blue_ui_style.update({
172149
# # Line numbers.
173150
# Token.LineNumber: '#aa6666',
174151
#
@@ -192,4 +169,4 @@ def generate_style(python_style, ui_style):
192169
# Token.Menu.Completions.Meta.Current: 'bg:#00aaaa #000000',
193170
# Token.Menu.Completions.ProgressBar: 'bg:#aaaaaa',
194171
# Token.Menu.Completions.ProgressButton: 'bg:#000000',
195-
#})
172+
# })

‎ptpython/utils.py

+45-31
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,39 @@
11
"""
22
For internal use only.
33
"""
4-
from __future__ import unicode_literals
5-
6-
from prompt_toolkit.mouse_events import MouseEventType
74
import re
5+
from typing import Callable, TypeVar, cast
6+
7+
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
88

9-
__all__ = (
10-
'has_unclosed_brackets',
11-
'get_jedi_script_from_document',
12-
'document_is_multiline_python',
13-
)
9+
__all__ = [
10+
"has_unclosed_brackets",
11+
"get_jedi_script_from_document",
12+
"document_is_multiline_python",
13+
]
1414

1515

16-
def has_unclosed_brackets(text):
16+
def has_unclosed_brackets(text: str) -> bool:
1717
"""
1818
Starting at the end of the string. If we find an opening bracket
1919
for which we didn't had a closing one yet, return True.
2020
"""
2121
stack = []
2222

2323
# Ignore braces inside strings
24-
text = re.sub(r'''('[^']*'|"[^"]*")''', '', text) # XXX: handle escaped quotes.!
24+
text = re.sub(r"""('[^']*'|"[^"]*")""", "", text) # XXX: handle escaped quotes.!
2525

2626
for c in reversed(text):
27-
if c in '])}':
27+
if c in "])}":
2828
stack.append(c)
2929

30-
elif c in '[({':
30+
elif c in "[({":
3131
if stack:
32-
if ((c == '[' and stack[-1] == ']') or
33-
(c == '{' and stack[-1] == '}') or
34-
(c == '(' and stack[-1] == ')')):
32+
if (
33+
(c == "[" and stack[-1] == "]")
34+
or (c == "{" and stack[-1] == "}")
35+
or (c == "(" and stack[-1] == ")")
36+
):
3537
stack.pop()
3638
else:
3739
# Opening bracket for which we didn't had a closing one.
@@ -42,15 +44,17 @@ def has_unclosed_brackets(text):
4244

4345
def get_jedi_script_from_document(document, locals, globals):
4446
import jedi # We keep this import in-line, to improve start-up time.
45-
# Importing Jedi is 'slow'.
47+
48+
# Importing Jedi is 'slow'.
4649

4750
try:
4851
return jedi.Interpreter(
4952
document.text,
5053
column=document.cursor_position_col,
5154
line=document.cursor_position_row + 1,
52-
path='input-text',
53-
namespaces=[locals, globals])
55+
path="input-text",
56+
namespaces=[locals, globals],
57+
)
5458
except ValueError:
5559
# Invalid cursor position.
5660
# ValueError('`column` parameter is not in a valid range.')
@@ -70,14 +74,15 @@ def get_jedi_script_from_document(document, locals, globals):
7074
return None
7175

7276

73-
_multiline_string_delims = re.compile('''[']{3}|["]{3}''')
77+
_multiline_string_delims = re.compile("""[']{3}|["]{3}""")
7478

7579

7680
def document_is_multiline_python(document):
7781
"""
7882
Determine whether this is a multiline Python document.
7983
"""
80-
def ends_in_multiline_string():
84+
85+
def ends_in_multiline_string() -> bool:
8186
"""
8287
``True`` if we're inside a multiline string at the end of the text.
8388
"""
@@ -90,38 +95,47 @@ def ends_in_multiline_string():
9095
opening = None
9196
return bool(opening)
9297

93-
if '\n' in document.text or ends_in_multiline_string():
98+
if "\n" in document.text or ends_in_multiline_string():
9499
return True
95100

96-
def line_ends_with_colon():
97-
return document.current_line.rstrip()[-1:] == ':'
101+
def line_ends_with_colon() -> bool:
102+
return document.current_line.rstrip()[-1:] == ":"
98103

99104
# If we just typed a colon, or still have open brackets, always insert a real newline.
100-
if line_ends_with_colon() or \
101-
(document.is_cursor_at_the_end and
102-
has_unclosed_brackets(document.text_before_cursor)) or \
103-
document.text.startswith('@'):
105+
if (
106+
line_ends_with_colon()
107+
or (
108+
document.is_cursor_at_the_end
109+
and has_unclosed_brackets(document.text_before_cursor)
110+
)
111+
or document.text.startswith("@")
112+
):
104113
return True
105114

106115
# If the character before the cursor is a backslash (line continuation
107116
# char), insert a new line.
108-
elif document.text_before_cursor[-1:] == '\\':
117+
elif document.text_before_cursor[-1:] == "\\":
109118
return True
110119

111120
return False
112121

113122

114-
def if_mousedown(handler):
123+
_T = TypeVar("_T", bound=Callable[[MouseEvent], None])
124+
125+
126+
def if_mousedown(handler: _T) -> _T:
115127
"""
116128
Decorator for mouse handlers.
117129
Only handle event when the user pressed mouse down.
118130
119131
(When applied to a token list. Scroll events will bubble up and are handled
120132
by the Window.)
121133
"""
122-
def handle_if_mouse_down(mouse_event):
134+
135+
def handle_if_mouse_down(mouse_event: MouseEvent):
123136
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
124137
return handler(mouse_event)
125138
else:
126139
return NotImplemented
127-
return handle_if_mouse_down
140+
141+
return cast(_T, handle_if_mouse_down)

‎ptpython/validator.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
from __future__ import unicode_literals
1+
from prompt_toolkit.validation import ValidationError, Validator
22

3-
from prompt_toolkit.validation import Validator, ValidationError
3+
__all__ = ["PythonValidator"]
44

5-
__all__ = (
6-
'PythonValidator',
7-
)
85

96
class PythonValidator(Validator):
107
"""
@@ -13,6 +10,7 @@ class PythonValidator(Validator):
1310
:param get_compiler_flags: Callable that returns the currently
1411
active compiler flags.
1512
"""
13+
1614
def __init__(self, get_compiler_flags=None):
1715
self.get_compiler_flags = get_compiler_flags
1816

@@ -22,7 +20,7 @@ def validate(self, document):
2220
"""
2321
# When the input starts with Ctrl-Z, always accept. This means EOF in a
2422
# Python REPL.
25-
if document.text.startswith('\x1a'):
23+
if document.text.startswith("\x1a"):
2624
return
2725

2826
try:
@@ -31,17 +29,19 @@ def validate(self, document):
3129
else:
3230
flags = 0
3331

34-
compile(document.text, '<input>', 'exec', flags=flags, dont_inherit=True)
32+
compile(document.text, "<input>", "exec", flags=flags, dont_inherit=True)
3533
except SyntaxError as e:
3634
# Note, the 'or 1' for offset is required because Python 2.7
3735
# gives `None` as offset in case of '4=4' as input. (Looks like
3836
# fixed in Python 3.)
39-
index = document.translate_row_col_to_index(e.lineno - 1, (e.offset or 1) - 1)
40-
raise ValidationError(index, 'Syntax Error')
37+
index = document.translate_row_col_to_index(
38+
e.lineno - 1, (e.offset or 1) - 1
39+
)
40+
raise ValidationError(index, "Syntax Error")
4141
except TypeError as e:
4242
# e.g. "compile() expected string without null bytes"
4343
raise ValidationError(0, str(e))
4444
except ValueError as e:
4545
# In Python 2, compiling "\x9" (an invalid escape sequence) raises
4646
# ValueError instead of SyntaxError.
47-
raise ValidationError(0, 'Syntax Error: %s' % e)
47+
raise ValidationError(0, "Syntax Error: %s" % e)

‎pyproject.toml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[tool.black]
2+
target-version = ['py36']
3+
4+
5+
[tool.isort]
6+
# isort configuration that is compatible with Black.
7+
multi_line_output = 3
8+
include_trailing_comma = true
9+
known_first_party = "ptpython"
10+
known_third_party = "prompt_toolkit,pygments,asyncssh"
11+
force_grid_wrap = 0
12+
use_parentheses = true
13+
line_length = 88

‎setup.py

+32-27
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,48 @@
11
#!/usr/bin/env python
22
import os
33
import sys
4-
from setuptools import setup, find_packages
54

6-
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f:
5+
from setuptools import find_packages, setup
6+
7+
with open(os.path.join(os.path.dirname(__file__), "README.rst")) as f:
78
long_description = f.read()
89

910

1011
setup(
11-
name='ptpython',
12-
author='Jonathan Slenders',
13-
version='2.0.5',
14-
url='https://github.com/jonathanslenders/ptpython',
15-
description='Python REPL build on top of prompt_toolkit',
12+
name="ptpython",
13+
author="Jonathan Slenders",
14+
version="2.0.5",
15+
url="https://github.com/prompt-toolkit/ptpython",
16+
description="Python REPL build on top of prompt_toolkit",
1617
long_description=long_description,
17-
packages=find_packages('.'),
18-
install_requires = [
19-
'appdirs',
20-
'docopt',
21-
'jedi>=0.9.0',
22-
'prompt_toolkit>=2.0.8,<2.1.0',
23-
'pygments',
18+
packages=find_packages("."),
19+
install_requires=[
20+
"appdirs",
21+
"jedi>=0.9.0",
22+
"prompt_toolkit>=3.0.0,<3.1.0",
23+
"pygments",
2424
],
25+
python_requires=">=3.6",
2526
classifiers=[
26-
'Programming Language :: Python',
27-
'Programming Language :: Python :: 3',
28-
'Programming Language :: Python :: 2',
27+
"Programming Language :: Python :: 3",
28+
"Programming Language :: Python :: 3.6",
29+
"Programming Language :: Python :: 3.7",
30+
"Programming Language :: Python :: 3.8",
31+
"Programming Language :: Python :: 3 :: Only",
32+
"Programming Language :: Python",
2933
],
3034
entry_points={
31-
'console_scripts': [
32-
'ptpython = ptpython.entry_points.run_ptpython:run',
33-
'ptipython = ptpython.entry_points.run_ptipython:run',
34-
'ptpython%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[0],
35-
'ptpython%s.%s = ptpython.entry_points.run_ptpython:run' % sys.version_info[:2],
36-
'ptipython%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[0],
37-
'ptipython%s.%s = ptpython.entry_points.run_ptipython:run' % sys.version_info[:2],
35+
"console_scripts": [
36+
"ptpython = ptpython.entry_points.run_ptpython:run",
37+
"ptipython = ptpython.entry_points.run_ptipython:run",
38+
"ptpython%s = ptpython.entry_points.run_ptpython:run" % sys.version_info[0],
39+
"ptpython%s.%s = ptpython.entry_points.run_ptpython:run"
40+
% sys.version_info[:2],
41+
"ptipython%s = ptpython.entry_points.run_ptipython:run"
42+
% sys.version_info[0],
43+
"ptipython%s.%s = ptpython.entry_points.run_ptipython:run"
44+
% sys.version_info[:2],
3845
]
3946
},
40-
extras_require={
41-
'ptipython': ['ipython'] # For ptipython, we need to have IPython
42-
}
47+
extras_require={"ptipython": ["ipython"]}, # For ptipython, we need to have IPython
4348
)

‎tests/run_tests.py

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
#!/usr/bin/env python
2-
from __future__ import unicode_literals
3-
42
import unittest
53

6-
7-
# For now there are no tests here.
8-
# However this is sufficient for Travis to do at least a syntax check.
9-
# That way we are at least sure to restrict to the Python 2.6 syntax.
10-
114
import ptpython.completer
12-
import ptpython.filters
13-
#import ptpython.ipython
145
import ptpython.eventloop
6+
import ptpython.filters
157
import ptpython.history_browser
168
import ptpython.key_bindings
179
import ptpython.layout
@@ -21,6 +13,10 @@
2113
import ptpython.utils
2214
import ptpython.validator
2315

16+
# For now there are no tests here.
17+
# However this is sufficient for Travis to do at least a syntax check.
18+
# That way we are at least sure to restrict to the Python 2.6 syntax.
19+
2420

25-
if __name__ == '__main__':
21+
if __name__ == "__main__":
2622
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.