Skip to content

bpo-37903: IDLE: Shell sidebar with prompts (take #2) #22682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 68 commits into from
Apr 28, 2021

Conversation

taleinat
Copy link
Contributor

@taleinat taleinat commented Oct 13, 2020

(Take 2 of this PR, following up PR GH-15474, just to have a cleaner history and conversation, as per @terryjreedy's request.)

This is a working implementation of a sidebar for IDLE's shell, removing the prompts from the main text widget (except for special prompts such as the one used by the debugger.) It works very well in my testing.

Currently known to be missing:

  1. handling of left mouse button clicks/drags on the sidebar

Notes on the implementation:

  • I mostly made each change in a separate commit to make reviewing easier.
  • This does not use the same base-class as the editor line-numbers sidebar, because it uses a canvas widget rather than a text widget. This is needed due to the shell soft-wrapping lines, causing lines to have varied height. Also, Squeezer inserts buttons into the shell's text window, and those lines have a different height as well. (I will refactor these to use a common base-class in a future PR.)
  • This includes quite a bit of work to get the "stdin" tag applied consistently to user input, which was rather broken but not relied on for anything. (Actually, this fixes the "recall" functionality in some edge cases.)
  • Removing prompts from the text widget made knowing where each statement started impossible, but the sidebar needs to know this. This PR adds the "console" tag to the newline before each statement to overcome this.
  • Removing prompts has other side effects which needed to be addressed. For example, successive statements without output now create a single long range of input tagged "stdin", which required some changes to the recall() method.

https://bugs.python.org/issue37903

Specifically, avoid EditorWindow.ResetColorizer() moving the undo
and colorizer delegators to the top of the percolator, by removing
and inserting them.
This avoids ColorDelegator removing unrelated tags, specifically "stdin",
when re-colorizing.
Prompts are now marked in the text by adding the "console" tag to
the newline character ('\n') before the beginning of the line.  This
works around issues with the prompt itself usually no longer being
written in the text widget.

With another small fix to the PyShell.recall() method, input lines
can now be recognized consistently by checking for the "stdin" tag.

This is much better than attempting to separately track the prompt
and input lines, since those are difficult to keep up to date after
undo/redo actions and squeezing/unsqueezing of previous output.
The background color still uses that configured for line numbers.
The fix needed was for History to properly set the "stdin" tag
on text it inserts.
@terryjreedy
Copy link
Member

I verified that the double return bug is gone. I will recheck other stuff tomorrow. And also look at the test failure. If the difference of behavior on Windows does not affect Shell's behavior, then maybe the test is not needed.

With the main blocker I know of gone, I am hopeful we can get this in the May 3 beta. It would not have to be perfect because I do not expect to immediately backport for the 3.9.5 and 3.8.10 releases on the same day. (Hence, I removed the automatic backport labels.) At this point, I don't mind not putting this in 3.8 (May 4 is its last maintenance release), or delaying putting it in 3.9 (next is 3.9.6 on June 26) until tested and improved. In particular, I would want tab indents gone so users see an immediate benefit that merits having their visual comfort initially disrupted.

If you pull a couple of things into separate issues and PRs, then I will deal with news and merging, once satisfied, and baby-sitting the backports.

My thinking now is that even if a bug is discovered during and its quick fix is motivated by another issue, it should be done as a separate issue and PR as long as it is applicable to and an upgrade to IDLE as it is. (In otherwords, if we would want it even without the main issue.) Then merge it into the original PR. I should have said something more earlier.

@taleinat
Copy link
Contributor Author

@terryjreedy, I suggest that you wait and let me take another look at the tests. I should find the time for that in the next few days, and hopefully will find a solution that works.

If I don't, we can indeed temporarily disable the offending tests, since the issue is entirely with the testing method being fragile (rather than an actual problem with IDLE.)

AFAICT after reviewing everything here and the latest fixes and cleanups, there are no other outstanding issues, and there is no need for separate PRs.

Copy link
Member

@terryjreedy terryjreedy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name changes and equivalent code replacement.

@@ -370,5 +391,347 @@ def assert_colors_are_equal(colors):
assert_colors_are_equal(orig_colors)


def test_coroutine(test_method):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two issues with the name. First, I am conditioned to see test_x as a test function. Second, in usual Python parlance, the generators returned by the test generator functions (called semi-coroutines by Tim Peters) are neither 'coroutines' (defined with 'async def', https://docs.python.org/3/glossary.html) nor are they being used as such (in an async context). I prefer 'wrap_generator' and 'self.run_generator' and believe that this would be clearer to others also.

I think a doc string is also needed explaining why and what. I believe 'why' is that while some previous tests required root, to create and update widgets, these are the first tests requiring mainloop in order for widgets to properly respond to events not generated by test code but by the tested code.

I presume we can use the same machinery for shell tests that do not involve the side bar. I appreciate you 'pioneering' this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've renamed this to run_in_tk_mainloop() and extracted it to a separate tkinter_testing_utils module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also changed the wording to reference "generator functions" rather than "coroutines", and added more info in a doc-string and comments.

Comment on lines 408 to 419
try:
orig_use_subprocess = idlelib.pyshell.use_subprocess
except AttributeError:
orig_use_subprocess = None
idlelib.pyshell.use_subprocess = False
def cleanup_use_subprocess():
if orig_use_subprocess is not None:
idlelib.pyshell.use_subprocess = orig_use_subprocess
else:
del idlelib.pyshell.use_subprocess
cls.addClassCleanup(cleanup_use_subprocess)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try:
orig_use_subprocess = idlelib.pyshell.use_subprocess
except AttributeError:
orig_use_subprocess = None
idlelib.pyshell.use_subprocess = False
def cleanup_use_subprocess():
if orig_use_subprocess is not None:
idlelib.pyshell.use_subprocess = orig_use_subprocess
else:
del idlelib.pyshell.use_subprocess
cls.addClassCleanup(cleanup_use_subprocess)

Instead, add the following to pyshell, maybe line 62.
use_subprocess = False # Default for testing; defaults to True in main() for running.

text.event_generate('<<newline-and-indent>>')
text.insert('insert', line, 'stdin')

def run_test_coroutine(self, coroutine):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment above about 'coroutine'.

self.assert_sidebar_lines_end_with(['>>>', '>>>'])

@test_coroutine
def test_single_line_command(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python REPLs execute statements, not OS command lines ;-).

Suggested change
def test_single_line_command(self):
def test_single_line_statement(self):

self.assert_sidebar_lines_end_with(['>>>', None, '>>>'])

@test_coroutine
def test_multi_line_command(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_multi_line_command(self):
def test_multi_line_statement(self):


@test_coroutine
def test_multi_line_command(self):
# Block statements are not indented because IDLE auto-indents.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Block statements are not indented because IDLE auto-indents.
# Suite is not indented because IDLE auto-indents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a "suite" in this context? I'm not familiar with this use of the word.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indented statements after the header of a compound statement. A Python grammar term used throughout https://docs.python.org/3/reference/compound_stmts.html#compound-statements.

self.assert_sidebar_lines_synced()

@test_coroutine
def test_squeeze_single_line_command(self):
Copy link
Member

@terryjreedy terryjreedy Apr 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_squeeze_single_line_command(self):
def test_squeeze_single_line_statement(self):
# Test that statement is not squeezed.

Since not squeezed, there is not button. So why test button call below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this isn't testing much. I've improved this in the latest commit.

@terryjreedy
Copy link
Member

CONFIGDIALOG is disabled because it is still calling update_colors. That line must not be covered in its test. (probably should use some mock_funcs).

@taleinat
Copy link
Contributor Author

@terryjreedy
I have made the requested changes; please review again.

@terryjreedy
Copy link
Member

I am about to get more sleep before continuing. Ignoring the issue of making this work with #25678, discussed on the tracker, do you consider this PR ready to commit for a beta release?

@taleinat
Copy link
Contributor Author

taleinat commented Apr 28, 2021

do you consider this PR ready to commit for a beta release?

Yes!

I'd like to follow up quickly with implementing mouse interaction with the sidebar (similar to the line-numbers sidebar), including specifically a context-menu (i.e. right-click) option to copy the contents with prompts, which would be useful for doctests for example.

@gvanrossum
Copy link
Member

I checked this out and played with it, but while the prompt in the sidebar looks nice, indentation is still with tabs, which gives it a decidedly 1990s feel. :-)

@taleinat
Copy link
Contributor Author

@gvanrossum, thanks for giving this a whirl!

Changing indentation from tabs to spaces is a quick followup that @terryjreedy has begun working on (see PR GH-25678).

@taleinat
Copy link
Contributor Author

@terryjreedy, I've got a local branch which refactors ShellSidebar to also inherit from BaseSidebar, moves more common logic to BaseSidebar, and adds support for selection via mouse events to the shell sidebar. Let me know if you'd like those in this PR or in a followup PR.

@terryjreedy
Copy link
Member

#25678 works to change indents when applied on top of this. I have done a thorough manual testing of the combination. While I want to review these changes more, I am merging this now so we can move on. I will then merge master in #25678 and merge that. Given Tal preference, Guido's comments, and the fact that the changes here are, from a bug-finding viewpoint, more in need of user testing than those in #25678, the sidebar should be in place first. I will look at the new sidebar code after Tal posts a new PR. While trying to make the sidebar optional after adding a 'usesidebar' attribute, I will look at the pyshell changes. Tal, if I have trouble, I will ask for help.

@terryjreedy terryjreedy merged commit 15d3861 into python:master Apr 28, 2021
@bedevere-bot
Copy link

⚠️⚠️⚠️ Buildbot failure ⚠️⚠️⚠️

Hi! The buildbot s390x Fedora Clang 3.x has failed when building commit 15d3861.

What do you need to do:

  1. Don't panic.
  2. Check the buildbot page in the devguide if you don't know what the buildbots are or how they work.
  3. Go to the page of the buildbot that failed (https://buildbot.python.org/all/#builders/3/builds/65) and take a look at the build logs.
  4. Check if the failure is related to this commit (15d3861) or if it is a false positive.
  5. If the failure is related to this commit, please, reflect that on the issue and make a new Pull Request with a fix.

You can take a look at the buildbot page here:

https://buildbot.python.org/all/#builders/3/builds/65

Failed tests:

  • test_nntplib

Failed subtests:

  • test_with_statement - test.test_nntplib.NetworkedNNTP_SSLTests

Summary of the results of the build (if available):

== Tests result: FAILURE then FAILURE ==

410 tests OK.

10 slowest tests:

  • test_concurrent_futures: 2 min 39 sec
  • test_peg_generator: 2 min 23 sec
  • test_multiprocessing_spawn: 1 min 6 sec
  • test_multiprocessing_forkserver: 1 min
  • test_multiprocessing_fork: 54.5 sec
  • test_asyncio: 51.3 sec
  • test_signal: 47.3 sec
  • test_io: 33.8 sec
  • test_tokenize: 33.3 sec
  • test_unparse: 32.4 sec

1 test failed:
test_nntplib

15 tests skipped:
test_devpoll test_ioctl test_kqueue test_msilib test_nis
test_ossaudiodev test_readline test_startfile test_tix test_tk
test_ttk_guionly test_winconsoleio test_winreg test_winsound
test_zipfile64

1 re-run test:
test_nntplib

Total duration: 5 min 16 sec

Click to see traceback logs
Traceback (most recent call last):
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/test/test_nntplib.py", line 250, in wrapped
    meth(self)
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/test/test_nntplib.py", line 293, in test_with_statement
    if re.search(r'(?i)KEY.TOO.SMALL', ssl_err.reason):
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/re.py", line 178, in search
    return _compile(pattern, flags).search(string)
TypeError: expected string or bytes-like object


Traceback (most recent call last):
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/test/test_nntplib.py", line 277, in test_with_statement
    server = self.NNTP_CLASS(self.NNTP_HOST,
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/nntplib.py", line 1025, in __init__
    super().__init__(host, port, user, password, readermode,
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/nntplib.py", line 334, in __init__
    self.sock = self._create_socket(timeout)
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/nntplib.py", line 1031, in _create_socket
    sock = _encrypt_on(sock, self.ssl_context, self.host)
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/nntplib.py", line 292, in _encrypt_on
    return context.wrap_socket(sock, server_hostname=hostname)
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/ssl.py", line 518, in wrap_socket
    return self.sslsocket_class._create(
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/ssl.py", line 1070, in _create
    self.do_handshake()
  File "/home/dje/cpython-buildarea/3.x.edelsohn-fedora-z.clang/build/Lib/ssl.py", line 1339, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:969)

@taleinat taleinat deleted the idle-shell-sidebar-take2 branch May 4, 2021 09:22
@roseman roseman mannequin mentioned this pull request Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Stale PR or inactive for long period of time. type-feature A feature request or enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants