diff --git a/.azure-pipelines/windows-release.yml b/.azure-pipelines/windows-release.yml
new file mode 100644
index 00000000000000..3d072e3b43e17e
--- /dev/null
+++ b/.azure-pipelines/windows-release.yml
@@ -0,0 +1,129 @@
+name: Release_$(Build.SourceBranchName)_$(SourceTag)_$(Date:yyyyMMdd)$(Rev:.rr)
+
+variables:
+ __RealSigningCertificate: 'Python Software Foundation'
+# QUEUE TIME VARIABLES
+# GitRemote: python
+# SourceTag:
+# DoPGO: true
+# SigningCertificate: 'Python Software Foundation'
+# SigningDescription: 'Built: $(Build.BuildNumber)'
+# DoLayout: true
+# DoMSIX: true
+# DoNuget: true
+# DoEmbed: true
+# DoMSI: true
+# DoPublish: false
+# PyDotOrgUsername: ''
+# PyDotOrgServer: ''
+# BuildToPublish: ''
+
+trigger: none
+pr: none
+
+stages:
+- stage: Build
+ displayName: Build binaries
+ condition: and(succeeded(), not(variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-build.yml
+
+- stage: Sign
+ displayName: Sign binaries
+ dependsOn: Build
+ condition: and(succeeded(), not(variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-sign.yml
+
+- stage: Layout
+ displayName: Generate layouts
+ dependsOn: Sign
+ condition: and(succeeded(), not(variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-layout-full.yml
+ - template: windows-release/stage-layout-embed.yml
+ - template: windows-release/stage-layout-nuget.yml
+
+- stage: Pack
+ dependsOn: Layout
+ condition: and(succeeded(), not(variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-pack-nuget.yml
+
+- stage: Test
+ dependsOn: Pack
+ condition: and(succeeded(), not(variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-test-embed.yml
+ - template: windows-release/stage-test-nuget.yml
+
+- stage: Layout_MSIX
+ displayName: Generate MSIX layouts
+ dependsOn: Sign
+ condition: and(succeeded(), and(eq(variables['DoMSIX'], 'true'), not(variables['BuildToPublish'])))
+ jobs:
+ - template: windows-release/stage-layout-msix.yml
+
+- stage: Pack_MSIX
+ displayName: Package MSIX
+ dependsOn: Layout_MSIX
+ condition: and(succeeded(), not(variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-pack-msix.yml
+
+- stage: Build_MSI
+ displayName: Build MSI installer
+ dependsOn: Sign
+ condition: and(succeeded(), and(eq(variables['DoMSI'], 'true'), not(variables['BuildToPublish'])))
+ jobs:
+ - template: windows-release/stage-msi.yml
+
+- stage: Test_MSI
+ displayName: Test MSI installer
+ dependsOn: Build_MSI
+ condition: and(succeeded(), not(variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-test-msi.yml
+
+- stage: PublishPyDotOrg
+ displayName: Publish to python.org
+ dependsOn: ['Test_MSI', 'Test']
+ condition: and(succeeded(), and(eq(variables['DoPublish'], 'true'), not(variables['BuildToPublish'])))
+ jobs:
+ - template: windows-release/stage-publish-pythonorg.yml
+
+- stage: PublishNuget
+ displayName: Publish to nuget.org
+ dependsOn: Test
+ condition: and(succeeded(), and(eq(variables['DoPublish'], 'true'), not(variables['BuildToPublish'])))
+ jobs:
+ - template: windows-release/stage-publish-nugetorg.yml
+
+- stage: PublishStore
+ displayName: Publish to Store
+ dependsOn: Pack_MSIX
+ condition: and(succeeded(), and(eq(variables['DoPublish'], 'true'), not(variables['BuildToPublish'])))
+ jobs:
+ - template: windows-release/stage-publish-store.yml
+
+
+- stage: PublishExistingPyDotOrg
+ displayName: Publish existing build to python.org
+ dependsOn: []
+ condition: and(succeeded(), and(eq(variables['DoPublish'], 'true'), variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-publish-pythonorg.yml
+
+- stage: PublishExistingNuget
+ displayName: Publish existing build to nuget.org
+ dependsOn: []
+ condition: and(succeeded(), and(eq(variables['DoPublish'], 'true'), variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-publish-nugetorg.yml
+
+- stage: PublishExistingStore
+ displayName: Publish existing build to Store
+ dependsOn: []
+ condition: and(succeeded(), and(eq(variables['DoPublish'], 'true'), variables['BuildToPublish']))
+ jobs:
+ - template: windows-release/stage-publish-store.yml
diff --git a/.azure-pipelines/windows-release/build-steps.yml b/.azure-pipelines/windows-release/build-steps.yml
new file mode 100644
index 00000000000000..d4563cd0d722c1
--- /dev/null
+++ b/.azure-pipelines/windows-release/build-steps.yml
@@ -0,0 +1,83 @@
+parameters:
+ ShouldPGO: false
+
+steps:
+- template: ./checkout.yml
+
+- powershell: |
+ $d = (.\PCbuild\build.bat -V) | %{ if($_ -match '\s+(\w+):\s*(.+)\s*$') { @{$Matches[1] = $Matches[2];} }};
+ Write-Host "##vso[task.setvariable variable=VersionText]$($d.PythonVersion)"
+ Write-Host "##vso[task.setvariable variable=VersionNumber]$($d.PythonVersionNumber)"
+ Write-Host "##vso[task.setvariable variable=VersionHex]$($d.PythonVersionHex)"
+ Write-Host "##vso[task.setvariable variable=VersionUnique]$($d.PythonVersionUnique)"
+ Write-Host "##vso[build.addbuildtag]$($d.PythonVersion)"
+ Write-Host "##vso[build.addbuildtag]$($d.PythonVersion)-$(Name)"
+ displayName: 'Extract version numbers'
+
+- ${{ if eq(parameters.ShouldPGO, 'false') }}:
+ - powershell: |
+ $env:SigningCertificate = $null
+ .\PCbuild\build.bat -v -p $(Platform) -c $(Configuration)
+ displayName: 'Run build'
+ env:
+ IncludeUwp: true
+ Py_OutDir: '$(Build.BinariesDirectory)\bin'
+
+- ${{ if eq(parameters.ShouldPGO, 'true') }}:
+ - powershell: |
+ $env:SigningCertificate = $null
+ .\PCbuild\build.bat -v -p $(Platform) --pgo
+ displayName: 'Run build with PGO'
+ env:
+ IncludeUwp: true
+ Py_OutDir: '$(Build.BinariesDirectory)\bin'
+
+- powershell: |
+ $kitroot = (gp 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots\').KitsRoot10
+ $tool = (gci -r "$kitroot\Bin\*\x64\signtool.exe" | sort FullName -Desc | select -First 1)
+ if (-not $tool) {
+ throw "SDK is not available"
+ }
+ Write-Host "##vso[task.prependpath]$($tool.Directory)"
+ displayName: 'Add WinSDK tools to path'
+
+- powershell: |
+ $env:SigningCertificate = $null
+ .\python.bat PC\layout -vv -t "$(Build.BinariesDirectory)\catalog" --catalog "${env:CAT}.cdf" --preset-default
+ makecat "${env:CAT}.cdf"
+ del "${env:CAT}.cdf"
+ if (-not (Test-Path "${env:CAT}.cat")) {
+ throw "Failed to build catalog file"
+ }
+ displayName: 'Generate catalog'
+ env:
+ CAT: $(Build.BinariesDirectory)\bin\$(Arch)\python
+
+- task: PublishPipelineArtifact@0
+ displayName: 'Publish binaries'
+ condition: and(succeeded(), not(and(eq(variables['Configuration'], 'Release'), variables['SigningCertificate'])))
+ inputs:
+ targetPath: '$(Build.BinariesDirectory)\bin\$(Arch)'
+ artifactName: bin_$(Name)
+
+- task: PublishPipelineArtifact@0
+ displayName: 'Publish binaries for signing'
+ condition: and(succeeded(), and(eq(variables['Configuration'], 'Release'), variables['SigningCertificate']))
+ inputs:
+ targetPath: '$(Build.BinariesDirectory)\bin\$(Arch)'
+ artifactName: unsigned_bin_$(Name)
+
+- task: CopyFiles@2
+ displayName: 'Layout Artifact: symbols'
+ inputs:
+ sourceFolder: $(Build.BinariesDirectory)\bin\$(Arch)
+ targetFolder: $(Build.ArtifactStagingDirectory)\symbols\$(Name)
+ flatten: true
+ contents: |
+ **\*.pdb
+
+- task: PublishBuildArtifacts@1
+ displayName: 'Publish Artifact: symbols'
+ inputs:
+ PathToPublish: '$(Build.ArtifactStagingDirectory)\symbols'
+ ArtifactName: symbols
diff --git a/.azure-pipelines/windows-release/checkout.yml b/.azure-pipelines/windows-release/checkout.yml
new file mode 100644
index 00000000000000..d42d55fff08dda
--- /dev/null
+++ b/.azure-pipelines/windows-release/checkout.yml
@@ -0,0 +1,21 @@
+parameters:
+ depth: 3
+
+steps:
+- checkout: none
+
+- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch https://github.com/$(GitRemote)/cpython.git .
+ displayName: 'git clone ($(GitRemote)/$(SourceTag))'
+ condition: and(succeeded(), and(variables['GitRemote'], variables['SourceTag']))
+
+- script: git clone --progress -v --depth ${{ parameters.depth }} --branch $(SourceTag) --single-branch $(Build.Repository.Uri) .
+ displayName: 'git clone (%s
' % (color, status)
+ self.textedit.appendHtml(s)
+
+ @Slot()
+ def manual_update(self):
+ # This function uses the formatted message passed in, but also uses
+ # information from the record to format the message in an appropriate
+ # color according to its severity (level).
+ level = random.choice(LEVELS)
+ extra = {'qThreadName': ctname() }
+ logger.log(level, 'Manually logged!', extra=extra)
+
+ @Slot()
+ def clear_display(self):
+ self.textedit.clear()
+
+
+ def main():
+ QtCore.QThread.currentThread().setObjectName('MainThread')
+ logging.getLogger().setLevel(logging.DEBUG)
+ app = QtWidgets.QApplication(sys.argv)
+ example = Window(app)
+ example.show()
+ sys.exit(app.exec_())
+
+ if __name__=='__main__':
+ main()
diff --git a/Doc/howto/unicode.rst b/Doc/howto/unicode.rst
index 24c3235e4add94..51bd64bfc232ca 100644
--- a/Doc/howto/unicode.rst
+++ b/Doc/howto/unicode.rst
@@ -57,14 +57,14 @@ their corresponding code points:
...
007B '{'; LEFT CURLY BRACKET
...
- 2167 'Ⅶ': ROMAN NUMERAL EIGHT
- 2168 'Ⅸ': ROMAN NUMERAL NINE
+ 2167 'Ⅷ'; ROMAN NUMERAL EIGHT
+ 2168 'Ⅸ'; ROMAN NUMERAL NINE
...
- 265E '♞': BLACK CHESS KNIGHT
- 265F '♟': BLACK CHESS PAWN
+ 265E '♞'; BLACK CHESS KNIGHT
+ 265F '♟'; BLACK CHESS PAWN
...
- 1F600 '😀': GRINNING FACE
- 1F609 '😉': WINKING FACE
+ 1F600 '😀'; GRINNING FACE
+ 1F609 '😉'; WINKING FACE
...
Strictly, these definitions imply that it's meaningless to say 'this is
diff --git a/Doc/includes/email-mime.py b/Doc/includes/email-mime.py
index c610242f11f843..6af2be0b08a48d 100644
--- a/Doc/includes/email-mime.py
+++ b/Doc/includes/email-mime.py
@@ -14,7 +14,7 @@
# family = the list of all recipients' email addresses
msg['From'] = me
msg['To'] = ', '.join(family)
-msg.preamble = 'Our family reunion'
+msg.preamble = 'You will not see this in a MIME-aware mail reader.\n'
# Open the files in binary mode. Use imghdr to figure out the
# MIME subtype for each specific image.
diff --git a/Doc/library/_thread.rst b/Doc/library/_thread.rst
index 26568dcbf8f532..bd653ab32bb9c4 100644
--- a/Doc/library/_thread.rst
+++ b/Doc/library/_thread.rst
@@ -106,7 +106,7 @@ This module defines the following constants and functions:
Its value may be used to uniquely identify this particular thread system-wide
(until the thread terminates, after which the value may be recycled by the OS).
- .. availability:: Windows, FreeBSD, Linux, macOS, OpenBSD.
+ .. availability:: Windows, FreeBSD, Linux, macOS, OpenBSD, NetBSD, AIX.
.. versionadded:: 3.8
diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst
index b77a38ccd48577..368b1cfebf0568 100644
--- a/Doc/library/argparse.rst
+++ b/Doc/library/argparse.rst
@@ -182,6 +182,10 @@ ArgumentParser objects
.. versionchanged:: 3.5
*allow_abbrev* parameter was added.
+ .. versionchanged:: 3.8
+ In previous versions, *allow_abbrev* also disabled grouping of short
+ flags such as ``-vv`` to mean ``-v -v``.
+
The following sections describe how each of these are used.
@@ -1098,9 +1102,8 @@ container should match the type_ specified::
usage: doors.py [-h] {1,2,3}
doors.py: error: argument door: invalid choice: 4 (choose from 1, 2, 3)
-Any object that supports the ``in`` operator can be passed as the *choices*
-value, so :class:`dict` objects, :class:`set` objects, custom containers,
-etc. are all supported.
+Any container can be passed as the *choices* value, so :class:`list` objects,
+:class:`set` objects, and custom containers are all supported.
required
diff --git a/Doc/library/array.rst b/Doc/library/array.rst
index 1f95dd61b9fcd7..2ae2a071262a17 100644
--- a/Doc/library/array.rst
+++ b/Doc/library/array.rst
@@ -36,9 +36,9 @@ defined:
+-----------+--------------------+-------------------+-----------------------+-------+
| ``'L'`` | unsigned long | int | 4 | |
+-----------+--------------------+-------------------+-----------------------+-------+
-| ``'q'`` | signed long long | int | 8 | \(2) |
+| ``'q'`` | signed long long | int | 8 | |
+-----------+--------------------+-------------------+-----------------------+-------+
-| ``'Q'`` | unsigned long long | int | 8 | \(2) |
+| ``'Q'`` | unsigned long long | int | 8 | |
+-----------+--------------------+-------------------+-----------------------+-------+
| ``'f'`` | float | float | 4 | |
+-----------+--------------------+-------------------+-----------------------+-------+
@@ -57,13 +57,6 @@ Notes:
.. deprecated-removed:: 3.3 4.0
-(2)
- The ``'q'`` and ``'Q'`` type codes are available only if
- the platform C compiler used to build Python supports C :c:type:`long long`,
- or, on Windows, :c:type:`__int64`.
-
- .. versionadded:: 3.3
-
The actual representation of values is determined by the machine architecture
(strictly speaking, by the C implementation). The actual size can be accessed
through the :attr:`itemsize` attribute.
@@ -83,7 +76,7 @@ The module defines the following type:
to add initial items to the array. Otherwise, the iterable initializer is
passed to the :meth:`extend` method.
- .. audit-event:: array.__new__ "typecode initializer"
+ .. audit-event:: array.__new__ typecode,initializer array.array
.. data:: typecodes
diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst
index 1884bea80e8047..92bf8912eb53dd 100644
--- a/Doc/library/ast.rst
+++ b/Doc/library/ast.rst
@@ -126,7 +126,7 @@ The abstract grammar is currently defined as follows:
Apart from the node classes, the :mod:`ast` module defines these utility functions
and classes for traversing abstract syntax trees:
-.. function:: parse(source, filename='{{ _('This Page') }}
Other Graphical User Interface Packages
@@ -912,7 +928,7 @@tags after a closed tag. + # Avoid extra lines, e.g. after
tags.
+ lastline = self.text.get('end-1c linestart', 'end-1c')
+ s = '\n\n' if lastline and not lastline.isspace() else '\n'
elif tag == 'span' and class_ == 'pre':
self.chartags = 'pre'
elif tag == 'span' and class_ == 'versionmodified':
@@ -99,7 +104,7 @@ def handle_starttag(self, tag, attrs):
elif tag == 'li':
s = '\n* ' if self.simplelist else '\n\n* '
elif tag == 'dt':
- s = '\n\n' if not self.nested_dl else '\n' # avoid extra line
+ s = '\n\n' if not self.nested_dl else '\n' # Avoid extra line.
self.nested_dl = False
elif tag == 'dd':
self.indent()
@@ -120,16 +125,18 @@ def handle_starttag(self, tag, attrs):
self.tags = tag
if self.show:
self.text.insert('end', s, (self.tags, self.chartags))
+ self.prevtag = (True, tag)
def handle_endtag(self, tag):
"Handle endtags in help.html."
if tag in ['h1', 'h2', 'h3']:
- self.indent(0) # clear tag, reset indent
+ assert self.level == 0
if self.show:
indent = (' ' if tag == 'h3' else
' ' if tag == 'h2' else
'')
self.toc.append((indent+self.header, self.text.index('insert')))
+ self.tags = ''
elif tag in ['span', 'em']:
self.chartags = ''
elif tag == 'a':
@@ -138,7 +145,8 @@ def handle_endtag(self, tag):
self.pre = False
self.tags = ''
elif tag in ['ul', 'dd', 'ol']:
- self.indent(amt=-1)
+ self.indent(-1)
+ self.prevtag = (False, tag)
def handle_data(self, data):
"Handle date segments in help.html."
@@ -163,7 +171,7 @@ def __init__(self, parent, filename):
"Configure tags and feed file to parser."
uwide = idleConf.GetOption('main', 'EditorWindow', 'width', type='int')
uhigh = idleConf.GetOption('main', 'EditorWindow', 'height', type='int')
- uhigh = 3 * uhigh // 4 # lines average 4/3 of editor line height
+ uhigh = 3 * uhigh // 4 # Lines average 4/3 of editor line height.
Text.__init__(self, parent, wrap='word', highlightthickness=0,
padx=5, borderwidth=0, width=uwide, height=uhigh)
@@ -203,7 +211,6 @@ class HelpFrame(Frame):
"Display html text, scrollbar, and toc."
def __init__(self, parent, filename):
Frame.__init__(self, parent)
- # keep references to widgets for test access.
self.text = text = HelpText(self, filename)
self['background'] = text['background']
self.toc = toc = self.toc_menu(text)
@@ -211,7 +218,7 @@ def __init__(self, parent, filename):
text['yscrollcommand'] = scroll.set
self.rowconfigure(0, weight=1)
- self.columnconfigure(1, weight=1) # text
+ self.columnconfigure(1, weight=1) # Only expand the text widget.
toc.grid(row=0, column=0, sticky='nw')
text.grid(row=0, column=1, sticky='nsew')
scroll.grid(row=0, column=2, sticky='ns')
@@ -257,7 +264,7 @@ def copy_strip():
same, help.html can be backported. The internal Python version
number is not displayed. If maintenance idle.rst diverges from
the master version, then instead of backporting help.html from
- master, repeat the proceedure above to generate a maintenance
+ master, repeat the procedure above to generate a maintenance
version.
"""
src = join(abspath(dirname(dirname(dirname(__file__)))),
@@ -273,7 +280,7 @@ def show_idlehelp(parent):
"Create HelpWindow; called from Idle Help event handler."
filename = join(abspath(dirname(__file__)), 'help.html')
if not isfile(filename):
- # try copy_strip, present message
+ # Try copy_strip, present message.
return
HelpWindow(parent, filename, 'IDLE Help (%s)' % python_version())
diff --git a/Lib/idlelib/history.py b/Lib/idlelib/history.py
index 56f53a0f2fb991..ad44a96a9de2c0 100644
--- a/Lib/idlelib/history.py
+++ b/Lib/idlelib/history.py
@@ -39,7 +39,7 @@ def history_prev(self, event):
return "break"
def fetch(self, reverse):
- '''Fetch statememt and replace current line in text widget.
+ '''Fetch statement and replace current line in text widget.
Set prefix and pointer as needed for successive fetches.
Reset them to None, None when returning to the start line.
diff --git a/Lib/idlelib/idle_test/htest.py b/Lib/idlelib/idle_test/htest.py
index 6e3398ed0bc8db..1373b7642a6ea9 100644
--- a/Lib/idlelib/idle_test/htest.py
+++ b/Lib/idlelib/idle_test/htest.py
@@ -67,6 +67,7 @@ def _wrapper(parent): # htest #
import idlelib.pyshell # Set Windows DPI awareness before Tk().
from importlib import import_module
+import textwrap
import tkinter as tk
from tkinter.ttk import Scrollbar
tk.NoDefaultRoot()
@@ -108,6 +109,16 @@ def _wrapper(parent): # htest #
"The default color scheme is in idlelib/config-highlight.def"
}
+CustomRun_spec = {
+ 'file': 'query',
+ 'kwds': {'title': 'Customize query.py Run',
+ '_htest': True},
+ 'msg': "Enter with or [Run]. Print valid entry to Shell\n"
+ "Arguments are parsed into a list\n"
+ "Mode is currently restart True or False\n"
+ "Close dialog with valid entry, , [Cancel], [X]"
+ }
+
ConfigDialog_spec = {
'file': 'configdialog',
'kwds': {'title': 'ConfigDialogTest',
@@ -195,6 +206,26 @@ def _wrapper(parent): # htest #
"Check that changes were saved by opening the file elsewhere."
}
+_linenumbers_drag_scrolling_spec = {
+ 'file': 'sidebar',
+ 'kwds': {},
+ 'msg': textwrap.dedent("""\
+ 1. Click on the line numbers and drag down below the edge of the
+ window, moving the mouse a bit and then leaving it there for a while.
+ The text and line numbers should gradually scroll down, with the
+ selection updated continuously.
+
+ 2. With the lines still selected, click on a line number above the
+ selected lines. Only the line whose number was clicked should be
+ selected.
+
+ 3. Repeat step #1, dragging to above the window. The text and line
+ numbers should gradually scroll up, with the selection updated
+ continuously.
+
+ 4. Repeat step #2, clicking a line number below the selection."""),
+ }
+
_multi_call_spec = {
'file': 'multicall',
'kwds': {},
@@ -325,7 +356,7 @@ def _wrapper(parent): # htest #
ViewWindow_spec = {
'file': 'textview',
'kwds': {'title': 'Test textview',
- 'text': 'The quick brown fox jumps over the lazy dog.\n'*35,
+ 'contents': 'The quick brown fox jumps over the lazy dog.\n'*35,
'_htest': True},
'msg': "Test for read-only property of text.\n"
"Select text, scroll window, close"
diff --git a/Lib/idlelib/idle_test/test_autocomplete.py b/Lib/idlelib/idle_test/test_autocomplete.py
index 6181b29ec250cc..2c478cd5c2a146 100644
--- a/Lib/idlelib/idle_test/test_autocomplete.py
+++ b/Lib/idlelib/idle_test/test_autocomplete.py
@@ -1,4 +1,4 @@
-"Test autocomplete, coverage 87%."
+"Test autocomplete, coverage 93%."
import unittest
from unittest.mock import Mock, patch
@@ -45,127 +45,177 @@ def setUp(self):
def test_init(self):
self.assertEqual(self.autocomplete.editwin, self.editor)
+ self.assertEqual(self.autocomplete.text, self.text)
def test_make_autocomplete_window(self):
testwin = self.autocomplete._make_autocomplete_window()
self.assertIsInstance(testwin, acw.AutoCompleteWindow)
def test_remove_autocomplete_window(self):
- self.autocomplete.autocompletewindow = (
- self.autocomplete._make_autocomplete_window())
- self.autocomplete._remove_autocomplete_window()
- self.assertIsNone(self.autocomplete.autocompletewindow)
+ acp = self.autocomplete
+ acp.autocompletewindow = m = Mock()
+ acp._remove_autocomplete_window()
+ m.hide_window.assert_called_once()
+ self.assertIsNone(acp.autocompletewindow)
def test_force_open_completions_event(self):
- # Test that force_open_completions_event calls _open_completions.
- o_cs = Func()
- self.autocomplete.open_completions = o_cs
- self.autocomplete.force_open_completions_event('event')
- self.assertEqual(o_cs.args, (True, False, True))
-
- def test_try_open_completions_event(self):
- Equal = self.assertEqual
- autocomplete = self.autocomplete
- trycompletions = self.autocomplete.try_open_completions_event
- o_c_l = Func()
- autocomplete._open_completions_later = o_c_l
-
- # _open_completions_later should not be called with no text in editor.
- trycompletions('event')
- Equal(o_c_l.args, None)
-
- # _open_completions_later should be called with COMPLETE_ATTRIBUTES (1).
- self.text.insert('1.0', 're.')
- trycompletions('event')
- Equal(o_c_l.args, (False, False, False, 1))
-
- # _open_completions_later should be called with COMPLETE_FILES (2).
- self.text.delete('1.0', 'end')
- self.text.insert('1.0', '"./Lib/')
- trycompletions('event')
- Equal(o_c_l.args, (False, False, False, 2))
+ # Call _open_completions and break.
+ acp = self.autocomplete
+ open_c = Func()
+ acp.open_completions = open_c
+ self.assertEqual(acp.force_open_completions_event('event'), 'break')
+ self.assertEqual(open_c.args[0], ac.FORCE)
def test_autocomplete_event(self):
Equal = self.assertEqual
- autocomplete = self.autocomplete
+ acp = self.autocomplete
- # Test that the autocomplete event is ignored if user is pressing a
- # modifier key in addition to the tab key.
+ # Result of autocomplete event: If modified tab, None.
ev = Event(mc_state=True)
- self.assertIsNone(autocomplete.autocomplete_event(ev))
+ self.assertIsNone(acp.autocomplete_event(ev))
del ev.mc_state
- # Test that tab after whitespace is ignored.
+ # If tab after whitespace, None.
self.text.insert('1.0', ' """Docstring.\n ')
- self.assertIsNone(autocomplete.autocomplete_event(ev))
+ self.assertIsNone(acp.autocomplete_event(ev))
self.text.delete('1.0', 'end')
- # If autocomplete window is open, complete() method is called.
+ # If active autocomplete window, complete() and 'break'.
self.text.insert('1.0', 're.')
- # This must call autocomplete._make_autocomplete_window().
- Equal(self.autocomplete.autocomplete_event(ev), 'break')
-
- # If autocomplete window is not active or does not exist,
- # open_completions is called. Return depends on its return.
- autocomplete._remove_autocomplete_window()
- o_cs = Func() # .result = None.
- autocomplete.open_completions = o_cs
- Equal(self.autocomplete.autocomplete_event(ev), None)
- Equal(o_cs.args, (False, True, True))
- o_cs.result = True
- Equal(self.autocomplete.autocomplete_event(ev), 'break')
- Equal(o_cs.args, (False, True, True))
-
- def test_open_completions_later(self):
- # Test that autocomplete._delayed_completion_id is set.
+ acp.autocompletewindow = mock = Mock()
+ mock.is_active = Mock(return_value=True)
+ Equal(acp.autocomplete_event(ev), 'break')
+ mock.complete.assert_called_once()
+ acp.autocompletewindow = None
+
+ # If no active autocomplete window, open_completions(), None/break.
+ open_c = Func(result=False)
+ acp.open_completions = open_c
+ Equal(acp.autocomplete_event(ev), None)
+ Equal(open_c.args[0], ac.TAB)
+ open_c.result = True
+ Equal(acp.autocomplete_event(ev), 'break')
+ Equal(open_c.args[0], ac.TAB)
+
+ def test_try_open_completions_event(self):
+ Equal = self.assertEqual
+ text = self.text
acp = self.autocomplete
+ trycompletions = acp.try_open_completions_event
+ after = Func(result='after1')
+ acp.text.after = after
+
+ # If no text or trigger, after not called.
+ trycompletions()
+ Equal(after.called, 0)
+ text.insert('1.0', 're')
+ trycompletions()
+ Equal(after.called, 0)
+
+ # Attribute needed, no existing callback.
+ text.insert('insert', ' re.')
acp._delayed_completion_id = None
- acp._open_completions_later(False, False, False, ac.COMPLETE_ATTRIBUTES)
+ trycompletions()
+ Equal(acp._delayed_completion_index, text.index('insert'))
+ Equal(after.args,
+ (acp.popupwait, acp._delayed_open_completions, ac.TRY_A))
cb1 = acp._delayed_completion_id
- self.assertTrue(cb1.startswith('after'))
-
- # Test that cb1 is cancelled and cb2 is new.
- acp._open_completions_later(False, False, False, ac.COMPLETE_FILES)
- self.assertNotIn(cb1, self.root.tk.call('after', 'info'))
- cb2 = acp._delayed_completion_id
- self.assertTrue(cb2.startswith('after') and cb2 != cb1)
- self.text.after_cancel(cb2)
+ Equal(cb1, 'after1')
+
+ # File needed, existing callback cancelled.
+ text.insert('insert', ' "./Lib/')
+ after.result = 'after2'
+ cancel = Func()
+ acp.text.after_cancel = cancel
+ trycompletions()
+ Equal(acp._delayed_completion_index, text.index('insert'))
+ Equal(cancel.args, (cb1,))
+ Equal(after.args,
+ (acp.popupwait, acp._delayed_open_completions, ac.TRY_F))
+ Equal(acp._delayed_completion_id, 'after2')
def test_delayed_open_completions(self):
- # Test that autocomplete._delayed_completion_id set to None
- # and that open_completions is not called if the index is not
- # equal to _delayed_completion_index.
+ Equal = self.assertEqual
acp = self.autocomplete
- acp.open_completions = Func()
+ open_c = Func()
+ acp.open_completions = open_c
+ self.text.insert('1.0', '"dict.')
+
+ # Set autocomplete._delayed_completion_id to None.
+ # Text index changed, don't call open_completions.
acp._delayed_completion_id = 'after'
acp._delayed_completion_index = self.text.index('insert+1c')
- acp._delayed_open_completions(1, 2, 3)
+ acp._delayed_open_completions('dummy')
self.assertIsNone(acp._delayed_completion_id)
- self.assertEqual(acp.open_completions.called, 0)
+ Equal(open_c.called, 0)
- # Test that open_completions is called if indexes match.
+ # Text index unchanged, call open_completions.
acp._delayed_completion_index = self.text.index('insert')
- acp._delayed_open_completions(1, 2, 3, ac.COMPLETE_FILES)
- self.assertEqual(acp.open_completions.args, (1, 2, 3, 2))
+ acp._delayed_open_completions((1, 2, 3, ac.FILES))
+ self.assertEqual(open_c.args[0], (1, 2, 3, ac.FILES))
+
+ def test_oc_cancel_comment(self):
+ none = self.assertIsNone
+ acp = self.autocomplete
+
+ # Comment is in neither code or string.
+ acp._delayed_completion_id = 'after'
+ after = Func(result='after')
+ acp.text.after_cancel = after
+ self.text.insert(1.0, '# comment')
+ none(acp.open_completions(ac.TAB)) # From 'else' after 'elif'.
+ none(acp._delayed_completion_id)
+
+ def test_oc_no_list(self):
+ acp = self.autocomplete
+ fetch = Func(result=([],[]))
+ acp.fetch_completions = fetch
+ self.text.insert('1.0', 'object')
+ self.assertIsNone(acp.open_completions(ac.TAB))
+ self.text.insert('insert', '.')
+ self.assertIsNone(acp.open_completions(ac.TAB))
+ self.assertEqual(fetch.called, 2)
+
+
+ def test_open_completions_none(self):
+ # Test other two None returns.
+ none = self.assertIsNone
+ acp = self.autocomplete
+
+ # No object for attributes or need call not allowed.
+ self.text.insert(1.0, '.')
+ none(acp.open_completions(ac.TAB))
+ self.text.insert('insert', ' int().')
+ none(acp.open_completions(ac.TAB))
+
+ # Blank or quote trigger 'if complete ...'.
+ self.text.delete(1.0, 'end')
+ self.assertFalse(acp.open_completions(ac.TAB))
+ self.text.insert('1.0', '"')
+ self.assertFalse(acp.open_completions(ac.TAB))
+ self.text.delete('1.0', 'end')
+
+ class dummy_acw():
+ __init__ = Func()
+ show_window = Func(result=False)
+ hide_window = Func()
def test_open_completions(self):
- # Test completions of files and attributes as well as non-completion
- # of errors.
- self.text.insert('1.0', 'pr')
- self.assertTrue(self.autocomplete.open_completions(False, True, True))
+ # Test completions of files and attributes.
+ acp = self.autocomplete
+ fetch = Func(result=(['tem'],['tem', '_tem']))
+ acp.fetch_completions = fetch
+ def make_acw(): return self.dummy_acw()
+ acp._make_autocomplete_window = make_acw
+
+ self.text.insert('1.0', 'int.')
+ acp.open_completions(ac.TAB)
+ self.assertIsInstance(acp.autocompletewindow, self.dummy_acw)
self.text.delete('1.0', 'end')
# Test files.
self.text.insert('1.0', '"t')
- #self.assertTrue(self.autocomplete.open_completions(False, True, True))
- self.text.delete('1.0', 'end')
-
- # Test with blank will fail.
- self.assertFalse(self.autocomplete.open_completions(False, True, True))
-
- # Test with only string quote will fail.
- self.text.insert('1.0', '"')
- self.assertFalse(self.autocomplete.open_completions(False, True, True))
+ self.assertTrue(acp.open_completions(ac.TAB))
self.text.delete('1.0', 'end')
def test_fetch_completions(self):
@@ -174,21 +224,21 @@ def test_fetch_completions(self):
# a small list containing non-private variables.
# For file completion, a large list containing all files in the path,
# and a small list containing files that do not start with '.'.
- autocomplete = self.autocomplete
- small, large = self.autocomplete.fetch_completions(
- '', ac.COMPLETE_ATTRIBUTES)
+ acp = self.autocomplete
+ small, large = acp.fetch_completions(
+ '', ac.ATTRS)
if __main__.__file__ != ac.__file__:
self.assertNotIn('AutoComplete', small) # See issue 36405.
# Test attributes
- s, b = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES)
+ s, b = acp.fetch_completions('', ac.ATTRS)
self.assertLess(len(small), len(large))
self.assertTrue(all(filter(lambda x: x.startswith('_'), s)))
self.assertTrue(any(filter(lambda x: x.startswith('_'), b)))
# Test smalll should respect to __all__.
with patch.dict('__main__.__dict__', {'__all__': ['a', 'b']}):
- s, b = autocomplete.fetch_completions('', ac.COMPLETE_ATTRIBUTES)
+ s, b = acp.fetch_completions('', ac.ATTRS)
self.assertEqual(s, ['a', 'b'])
self.assertIn('__name__', b) # From __main__.__dict__
self.assertIn('sum', b) # From __main__.__builtins__.__dict__
@@ -197,7 +247,7 @@ def test_fetch_completions(self):
mock = Mock()
mock._private = Mock()
with patch.dict('__main__.__dict__', {'foo': mock}):
- s, b = autocomplete.fetch_completions('foo', ac.COMPLETE_ATTRIBUTES)
+ s, b = acp.fetch_completions('foo', ac.ATTRS)
self.assertNotIn('_private', s)
self.assertIn('_private', b)
self.assertEqual(s, [i for i in sorted(dir(mock)) if i[:1] != '_'])
@@ -211,36 +261,36 @@ def _listdir(path):
return ['monty', 'python', '.hidden']
with patch.object(os, 'listdir', _listdir):
- s, b = autocomplete.fetch_completions('', ac.COMPLETE_FILES)
+ s, b = acp.fetch_completions('', ac.FILES)
self.assertEqual(s, ['bar', 'foo'])
self.assertEqual(b, ['.hidden', 'bar', 'foo'])
- s, b = autocomplete.fetch_completions('~', ac.COMPLETE_FILES)
+ s, b = acp.fetch_completions('~', ac.FILES)
self.assertEqual(s, ['monty', 'python'])
self.assertEqual(b, ['.hidden', 'monty', 'python'])
def test_get_entity(self):
# Test that a name is in the namespace of sys.modules and
# __main__.__dict__.
- autocomplete = self.autocomplete
+ acp = self.autocomplete
Equal = self.assertEqual
- Equal(self.autocomplete.get_entity('int'), int)
+ Equal(acp.get_entity('int'), int)
# Test name from sys.modules.
mock = Mock()
with patch.dict('sys.modules', {'tempfile': mock}):
- Equal(autocomplete.get_entity('tempfile'), mock)
+ Equal(acp.get_entity('tempfile'), mock)
# Test name from __main__.__dict__.
di = {'foo': 10, 'bar': 20}
with patch.dict('__main__.__dict__', {'d': di}):
- Equal(autocomplete.get_entity('d'), di)
+ Equal(acp.get_entity('d'), di)
# Test name not in namespace.
with patch.dict('__main__.__dict__', {}):
with self.assertRaises(NameError):
- autocomplete.get_entity('not_exist')
+ acp.get_entity('not_exist')
if __name__ == '__main__':
diff --git a/Lib/idlelib/idle_test/test_autoexpand.py b/Lib/idlelib/idle_test/test_autoexpand.py
index e5f44c46871325..e734a8be714a2a 100644
--- a/Lib/idlelib/idle_test/test_autoexpand.py
+++ b/Lib/idlelib/idle_test/test_autoexpand.py
@@ -6,7 +6,7 @@
from tkinter import Text, Tk
-class Dummy_Editwin:
+class DummyEditwin:
# AutoExpand.__init__ only needs .text
def __init__(self, text):
self.text = text
@@ -18,7 +18,7 @@ def setUpClass(cls):
requires('gui')
cls.tk = Tk()
cls.text = Text(cls.tk)
- cls.auto_expand = AutoExpand(Dummy_Editwin(cls.text))
+ cls.auto_expand = AutoExpand(DummyEditwin(cls.text))
cls.auto_expand.bell = lambda: None
# If mock_tk.Text._decode understood indexes 'insert' with suffixed 'linestart',
diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py
index 833351bd799601..886959b17074f6 100644
--- a/Lib/idlelib/idle_test/test_calltip.py
+++ b/Lib/idlelib/idle_test/test_calltip.py
@@ -4,8 +4,7 @@
import unittest
import textwrap
import types
-
-default_tip = calltip._default_callable_argspec
+import re
# Test Class TC is used in multiple get_argspec test methods
@@ -28,6 +27,7 @@ def t6(no, self): 'doc'
t6.tip = "(no, self)"
def __call__(self, ci): 'doc'
__call__.tip = "(self, ci)"
+ def nd(self): pass # No doc.
# attaching .tip to wrapped methods does not work
@classmethod
def cm(cls, a): 'doc'
@@ -36,11 +36,12 @@ def sm(b): 'doc'
tc = TC()
-signature = calltip.get_argspec # 2.7 and 3.x use different functions
+default_tip = calltip._default_callable_argspec
+get_spec = calltip.get_argspec
-class Get_signatureTest(unittest.TestCase):
- # The signature function must return a string, even if blank.
+class Get_argspecTest(unittest.TestCase):
+ # The get_spec function must return a string, even if blank.
# Test a variety of objects to be sure that none cause it to raise
# (quite aside from getting as correct an answer as possible).
# The tests of builtins may break if inspect or the docstrings change,
@@ -49,57 +50,59 @@ class Get_signatureTest(unittest.TestCase):
def test_builtins(self):
+ def tiptest(obj, out):
+ self.assertEqual(get_spec(obj), out)
+
# Python class that inherits builtin methods
class List(list): "List() doc"
# Simulate builtin with no docstring for default tip test
class SB: __call__ = None
- def gtest(obj, out):
- self.assertEqual(signature(obj), out)
-
if List.__doc__ is not None:
- gtest(List, '(iterable=(), /)' + calltip._argument_positional
- + '\n' + List.__doc__)
- gtest(list.__new__,
+ tiptest(List,
+ f'(iterable=(), /){calltip._argument_positional}'
+ f'\n{List.__doc__}')
+ tiptest(list.__new__,
'(*args, **kwargs)\n'
'Create and return a new object. '
'See help(type) for accurate signature.')
- gtest(list.__init__,
+ tiptest(list.__init__,
'(self, /, *args, **kwargs)'
+ calltip._argument_positional + '\n' +
'Initialize self. See help(type(self)) for accurate signature.')
append_doc = (calltip._argument_positional
+ "\nAppend object to the end of the list.")
- gtest(list.append, '(self, object, /)' + append_doc)
- gtest(List.append, '(self, object, /)' + append_doc)
- gtest([].append, '(object, /)' + append_doc)
+ tiptest(list.append, '(self, object, /)' + append_doc)
+ tiptest(List.append, '(self, object, /)' + append_doc)
+ tiptest([].append, '(object, /)' + append_doc)
+
+ tiptest(types.MethodType, "method(function, instance)")
+ tiptest(SB(), default_tip)
- gtest(types.MethodType, "method(function, instance)")
- gtest(SB(), default_tip)
- import re
p = re.compile('')
- gtest(re.sub, '''\
+ tiptest(re.sub, '''\
(pattern, repl, string, count=0, flags=0)
Return the string obtained by replacing the leftmost
non-overlapping occurrences of the pattern in string by the
replacement repl. repl can be either a string or a callable;
if a string, backslash escapes in it are processed. If it is
a callable, it's passed the Match object and must return''')
- gtest(p.sub, '''\
+ tiptest(p.sub, '''\
(repl, string, count=0)
Return the string obtained by replacing the leftmost \
non-overlapping occurrences o...''')
def test_signature_wrap(self):
if textwrap.TextWrapper.__doc__ is not None:
- self.assertEqual(signature(textwrap.TextWrapper), '''\
+ self.assertEqual(get_spec(textwrap.TextWrapper), '''\
(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None,
placeholder=' [...]')''')
def test_properly_formated(self):
+
def foo(s='a'*100):
pass
@@ -112,35 +115,35 @@ def baz(s='a'*100, z='b'*100):
indent = calltip._INDENT
- str_foo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
- "aaaaaaaaaa')"
- str_bar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
- "aaaaaaaaaa')\nHello Guido"
- str_baz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
- "aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
- "bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
- "bbbbbbbbbbbbbbbbbbbbbb')"
-
- self.assertEqual(calltip.get_argspec(foo), str_foo)
- self.assertEqual(calltip.get_argspec(bar), str_bar)
- self.assertEqual(calltip.get_argspec(baz), str_baz)
+ sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
+ "aaaaaaaaaa')"
+ sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
+ "aaaaaaaaaa')\nHello Guido"
+ sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
+ "aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
+ "bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
+ "bbbbbbbbbbbbbbbbbbbbbb')"
+
+ for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]:
+ with self.subTest(func=func, doc=doc):
+ self.assertEqual(get_spec(func), doc)
def test_docline_truncation(self):
def f(): pass
f.__doc__ = 'a'*300
- self.assertEqual(signature(f), '()\n' + 'a' * (calltip._MAX_COLS-3) + '...')
+ self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}")
def test_multiline_docstring(self):
# Test fewer lines than max.
- self.assertEqual(signature(range),
+ self.assertEqual(get_spec(range),
"range(stop) -> range object\n"
"range(start, stop[, step]) -> range object")
# Test max lines
- self.assertEqual(signature(bytes), '''\
+ self.assertEqual(get_spec(bytes), '''\
bytes(iterable_of_ints) -> bytes
bytes(string, encoding[, errors]) -> bytes
bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
@@ -150,7 +153,7 @@ def test_multiline_docstring(self):
# Test more than max lines
def f(): pass
f.__doc__ = 'a\n' * 15
- self.assertEqual(signature(f), '()' + '\na' * calltip._MAX_LINES)
+ self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES)
def test_functions(self):
def t1(): 'doc'
@@ -166,14 +169,16 @@ def t5(a, b=None, *args, **kw): 'doc'
doc = '\ndoc' if t1.__doc__ is not None else ''
for func in (t1, t2, t3, t4, t5, TC):
- self.assertEqual(signature(func), func.tip + doc)
+ with self.subTest(func=func):
+ self.assertEqual(get_spec(func), func.tip + doc)
def test_methods(self):
doc = '\ndoc' if TC.__doc__ is not None else ''
for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
- self.assertEqual(signature(meth), meth.tip + doc)
- self.assertEqual(signature(TC.cm), "(a)" + doc)
- self.assertEqual(signature(TC.sm), "(b)" + doc)
+ with self.subTest(meth=meth):
+ self.assertEqual(get_spec(meth), meth.tip + doc)
+ self.assertEqual(get_spec(TC.cm), "(a)" + doc)
+ self.assertEqual(get_spec(TC.sm), "(b)" + doc)
def test_bound_methods(self):
# test that first parameter is correctly removed from argspec
@@ -181,7 +186,8 @@ def test_bound_methods(self):
for meth, mtip in ((tc.t1, "()"), (tc.t4, "(*args)"),
(tc.t6, "(self)"), (tc.__call__, '(ci)'),
(tc, '(ci)'), (TC.cm, "(a)"),):
- self.assertEqual(signature(meth), mtip + doc)
+ with self.subTest(meth=meth, mtip=mtip):
+ self.assertEqual(get_spec(meth), mtip + doc)
def test_starred_parameter(self):
# test that starred first parameter is *not* removed from argspec
@@ -189,17 +195,18 @@ class C:
def m1(*args): pass
c = C()
for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
- self.assertEqual(signature(meth), mtip)
+ with self.subTest(meth=meth, mtip=mtip):
+ self.assertEqual(get_spec(meth), mtip)
- def test_invalid_method_signature(self):
+ def test_invalid_method_get_spec(self):
class C:
def m2(**kwargs): pass
class Test:
def __call__(*, a): pass
mtip = calltip._invalid_method
- self.assertEqual(signature(C().m2), mtip)
- self.assertEqual(signature(Test()), mtip)
+ self.assertEqual(get_spec(C().m2), mtip)
+ self.assertEqual(get_spec(Test()), mtip)
def test_non_ascii_name(self):
# test that re works to delete a first parameter name that
@@ -208,12 +215,9 @@ def test_non_ascii_name(self):
assert calltip._first_param.sub('', uni) == '(a)'
def test_no_docstring(self):
- def nd(s):
- pass
- TC.nd = nd
- self.assertEqual(signature(nd), "(s)")
- self.assertEqual(signature(TC.nd), "(s)")
- self.assertEqual(signature(tc.nd), "()")
+ for meth, mtip in ((TC.nd, "(self)"), (tc.nd, "()")):
+ with self.subTest(meth=meth, mtip=mtip):
+ self.assertEqual(get_spec(meth), mtip)
def test_attribute_exception(self):
class NoCall:
@@ -229,11 +233,13 @@ def __call__(self, ci):
for meth, mtip in ((NoCall, default_tip), (CallA, default_tip),
(NoCall(), ''), (CallA(), '(a, b, c)'),
(CallB(), '(ci)')):
- self.assertEqual(signature(meth), mtip)
+ with self.subTest(meth=meth, mtip=mtip):
+ self.assertEqual(get_spec(meth), mtip)
def test_non_callables(self):
for obj in (0, 0.0, '0', b'0', [], {}):
- self.assertEqual(signature(obj), '')
+ with self.subTest(obj=obj):
+ self.assertEqual(get_spec(obj), '')
class Get_entityTest(unittest.TestCase):
diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py
index 6c6893580f42f6..3ec49e97af6f91 100644
--- a/Lib/idlelib/idle_test/test_codecontext.py
+++ b/Lib/idlelib/idle_test/test_codecontext.py
@@ -2,8 +2,9 @@
from idlelib import codecontext
import unittest
+import unittest.mock
from test.support import requires
-from tkinter import Tk, Frame, Text, TclError
+from tkinter import NSEW, Tk, Frame, Text, TclError
from unittest import mock
import re
@@ -42,6 +43,9 @@ def __init__(self, root, frame, text):
self.text = text
self.label = ''
+ def getlineno(self, index):
+ return int(float(self.text.index(index)))
+
def update_menu_label(self, **kwargs):
self.label = kwargs['label']
@@ -58,7 +62,7 @@ def setUpClass(cls):
text.insert('1.0', code_sample)
# Need to pack for creation of code context text widget.
frame.pack(side='left', fill='both', expand=1)
- text.pack(side='top', fill='both', expand=1)
+ text.grid(row=1, column=1, sticky=NSEW)
cls.editor = DummyEditwin(root, frame, text)
codecontext.idleConf.userCfg = testcfg
@@ -73,8 +77,29 @@ def tearDownClass(cls):
def setUp(self):
self.text.yview(0)
+ self.text['font'] = 'TkFixedFont'
self.cc = codecontext.CodeContext(self.editor)
+ self.highlight_cfg = {"background": '#abcdef',
+ "foreground": '#123456'}
+ orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight
+ def mock_idleconf_GetHighlight(theme, element):
+ if element == 'context':
+ return self.highlight_cfg
+ return orig_idleConf_GetHighlight(theme, element)
+ GetHighlight_patcher = unittest.mock.patch.object(
+ codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
+ GetHighlight_patcher.start()
+ self.addCleanup(GetHighlight_patcher.stop)
+
+ self.font_override = 'TkFixedFont'
+ def mock_idleconf_GetFont(root, configType, section):
+ return self.font_override
+ GetFont_patcher = unittest.mock.patch.object(
+ codecontext.idleConf, 'GetFont', mock_idleconf_GetFont)
+ GetFont_patcher.start()
+ self.addCleanup(GetFont_patcher.stop)
+
def tearDown(self):
if self.cc.context:
self.cc.context.destroy()
@@ -89,30 +114,24 @@ def test_init(self):
eq(cc.editwin, ed)
eq(cc.text, ed.text)
- eq(cc.textfont, ed.text['font'])
+ eq(cc.text['font'], ed.text['font'])
self.assertIsNone(cc.context)
eq(cc.info, [(0, -1, '', False)])
eq(cc.topvisible, 1)
- eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
- eq(self.root.tk.call('after', 'info', self.cc.t2)[1], 'timer')
+ self.assertIsNone(self.cc.t1)
def test_del(self):
self.cc.__del__()
- with self.assertRaises(TclError) as msg:
- self.root.tk.call('after', 'info', self.cc.t1)
- self.assertIn("doesn't exist", msg)
- with self.assertRaises(TclError) as msg:
- self.root.tk.call('after', 'info', self.cc.t2)
- self.assertIn("doesn't exist", msg)
- # For coverage on the except. Have to delete because the
- # above Tcl error is caught by after_cancel.
- del self.cc.t1, self.cc.t2
+
+ def test_del_with_timer(self):
+ timer = self.cc.t1 = self.text.after(10000, lambda: None)
self.cc.__del__()
+ with self.assertRaises(TclError) as cm:
+ self.root.tk.call('after', 'info', timer)
+ self.assertIn("doesn't exist", str(cm.exception))
def test_reload(self):
codecontext.CodeContext.reload()
- self.assertEqual(self.cc.colors, {'background': 'lightgray',
- 'foreground': '#000000'})
self.assertEqual(self.cc.context_depth, 15)
def test_toggle_code_context_event(self):
@@ -125,18 +144,31 @@ def test_toggle_code_context_event(self):
toggle()
# Toggle on.
- eq(toggle(), 'break')
+ toggle()
self.assertIsNotNone(cc.context)
- eq(cc.context['font'], cc.textfont)
- eq(cc.context['fg'], cc.colors['foreground'])
- eq(cc.context['bg'], cc.colors['background'])
+ eq(cc.context['font'], self.text['font'])
+ eq(cc.context['fg'], self.highlight_cfg['foreground'])
+ eq(cc.context['bg'], self.highlight_cfg['background'])
eq(cc.context.get('1.0', 'end-1c'), '')
eq(cc.editwin.label, 'Hide Code Context')
+ eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
# Toggle off.
- eq(toggle(), 'break')
+ toggle()
self.assertIsNone(cc.context)
eq(cc.editwin.label, 'Show Code Context')
+ self.assertIsNone(self.cc.t1)
+
+ # Scroll down and toggle back on.
+ line11_context = '\n'.join(x[2] for x in cc.get_context(11)[0])
+ cc.text.yview(11)
+ toggle()
+ eq(cc.context.get('1.0', 'end-1c'), line11_context)
+
+ # Toggle off and on again.
+ toggle()
+ toggle()
+ eq(cc.context.get('1.0', 'end-1c'), line11_context)
def test_get_context(self):
eq = self.assertEqual
@@ -227,7 +259,7 @@ def test_update_code_context(self):
(4, 4, ' def __init__(self, a, b):', 'def')])
eq(cc.topvisible, 5)
eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
- ' def __init__(self, a, b):')
+ ' def __init__(self, a, b):')
# Scroll down to line 11. Last 'def' is removed.
cc.text.yview(11)
@@ -239,9 +271,9 @@ def test_update_code_context(self):
(10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12)
eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
- ' def compare(self):\n'
- ' if a > b:\n'
- ' elif a < b:')
+ ' def compare(self):\n'
+ ' if a > b:\n'
+ ' elif a < b:')
# No scroll. No update, even though context_depth changed.
cc.update_code_context()
@@ -253,9 +285,9 @@ def test_update_code_context(self):
(10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12)
eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
- ' def compare(self):\n'
- ' if a > b:\n'
- ' elif a < b:')
+ ' def compare(self):\n'
+ ' if a > b:\n'
+ ' elif a < b:')
# Scroll up.
cc.text.yview(5)
@@ -276,7 +308,7 @@ def test_jumptoline(self):
cc.toggle_code_context_event()
# Empty context.
- cc.text.yview(f'{2}.0')
+ cc.text.yview('2.0')
cc.update_code_context()
eq(cc.topvisible, 2)
cc.context.mark_set('insert', '1.5')
@@ -284,7 +316,7 @@ def test_jumptoline(self):
eq(cc.topvisible, 1)
# 4 lines of context showing.
- cc.text.yview(f'{12}.0')
+ cc.text.yview('12.0')
cc.update_code_context()
eq(cc.topvisible, 12)
cc.context.mark_set('insert', '3.0')
@@ -293,7 +325,7 @@ def test_jumptoline(self):
# More context lines than limit.
cc.context_depth = 2
- cc.text.yview(f'{12}.0')
+ cc.text.yview('12.0')
cc.update_code_context()
eq(cc.topvisible, 12)
cc.context.mark_set('insert', '1.0')
@@ -313,56 +345,62 @@ def test_timer_event(self, mock_update):
self.cc.timer_event()
mock_update.assert_called()
- def test_config_timer_event(self):
+ def test_font(self):
eq = self.assertEqual
cc = self.cc
- save_font = cc.text['font']
- save_colors = codecontext.CodeContext.colors
- test_font = 'FakeFont'
+
+ orig_font = cc.text['font']
+ test_font = 'TkTextFont'
+ self.assertNotEqual(orig_font, test_font)
+
+ # Ensure code context is not active.
+ if cc.context is not None:
+ cc.toggle_code_context_event()
+
+ self.font_override = test_font
+ # Nothing breaks or changes with inactive code context.
+ cc.update_font()
+
+ # Activate code context, previous font change is immediately effective.
+ cc.toggle_code_context_event()
+ eq(cc.context['font'], test_font)
+
+ # Call the font update, change is picked up.
+ self.font_override = orig_font
+ cc.update_font()
+ eq(cc.context['font'], orig_font)
+
+ def test_highlight_colors(self):
+ eq = self.assertEqual
+ cc = self.cc
+
+ orig_colors = dict(self.highlight_cfg)
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
+ def assert_colors_are_equal(colors):
+ eq(cc.context['background'], colors['background'])
+ eq(cc.context['foreground'], colors['foreground'])
+
# Ensure code context is not active.
if cc.context:
cc.toggle_code_context_event()
- # Nothing updates on inactive code context.
- cc.text['font'] = test_font
- codecontext.CodeContext.colors = test_colors
- cc.config_timer_event()
- eq(cc.textfont, save_font)
- eq(cc.contextcolors, save_colors)
+ self.highlight_cfg = test_colors
+ # Nothing breaks with inactive code context.
+ cc.update_highlight_colors()
- # Activate code context, but no change to font or color.
+ # Activate code context, previous colors change is immediately effective.
cc.toggle_code_context_event()
- cc.text['font'] = save_font
- codecontext.CodeContext.colors = save_colors
- cc.config_timer_event()
- eq(cc.textfont, save_font)
- eq(cc.contextcolors, save_colors)
- eq(cc.context['font'], save_font)
- eq(cc.context['background'], save_colors['background'])
- eq(cc.context['foreground'], save_colors['foreground'])
-
- # Active code context, change font.
- cc.text['font'] = test_font
- cc.config_timer_event()
- eq(cc.textfont, test_font)
- eq(cc.contextcolors, save_colors)
- eq(cc.context['font'], test_font)
- eq(cc.context['background'], save_colors['background'])
- eq(cc.context['foreground'], save_colors['foreground'])
-
- # Active code context, change color.
- cc.text['font'] = save_font
- codecontext.CodeContext.colors = test_colors
- cc.config_timer_event()
- eq(cc.textfont, save_font)
- eq(cc.contextcolors, test_colors)
- eq(cc.context['font'], save_font)
- eq(cc.context['background'], test_colors['background'])
- eq(cc.context['foreground'], test_colors['foreground'])
- codecontext.CodeContext.colors = save_colors
- cc.config_timer_event()
+ assert_colors_are_equal(test_colors)
+
+ # Call colors update with no change to the configured colors.
+ cc.update_highlight_colors()
+ assert_colors_are_equal(test_colors)
+
+ # Call the colors update with code context active, change is picked up.
+ self.highlight_cfg = orig_colors
+ cc.update_highlight_colors()
+ assert_colors_are_equal(orig_colors)
class HelperFunctionText(unittest.TestCase):
diff --git a/Lib/idlelib/idle_test/test_config.py b/Lib/idlelib/idle_test/test_config.py
index 255210df7d9608..492f2f64892428 100644
--- a/Lib/idlelib/idle_test/test_config.py
+++ b/Lib/idlelib/idle_test/test_config.py
@@ -159,19 +159,6 @@ def test_is_empty(self):
self.assertFalse(parser.IsEmpty())
self.assertCountEqual(parser.sections(), ['Foo'])
- def test_remove_file(self):
- with tempfile.TemporaryDirectory() as tdir:
- path = os.path.join(tdir, 'test.cfg')
- parser = self.new_parser(path)
- parser.RemoveFile() # Should not raise exception.
-
- parser.AddSection('Foo')
- parser.SetOption('Foo', 'bar', 'true')
- parser.Save()
- self.assertTrue(os.path.exists(path))
- parser.RemoveFile()
- self.assertFalse(os.path.exists(path))
-
def test_save(self):
with tempfile.TemporaryDirectory() as tdir:
path = os.path.join(tdir, 'test.cfg')
diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py
index 12bc8473668334..4af4ff0242d740 100644
--- a/Lib/idlelib/idle_test/test_editor.py
+++ b/Lib/idlelib/idle_test/test_editor.py
@@ -42,5 +42,66 @@ class dummy():
self.assertEqual(func(dummy, inp), out)
+class TestGetLineIndent(unittest.TestCase):
+ def test_empty_lines(self):
+ for tabwidth in [1, 2, 4, 6, 8]:
+ for line in ['', '\n']:
+ with self.subTest(line=line, tabwidth=tabwidth):
+ self.assertEqual(
+ editor.get_line_indent(line, tabwidth=tabwidth),
+ (0, 0),
+ )
+
+ def test_tabwidth_4(self):
+ # (line, (raw, effective))
+ tests = (('no spaces', (0, 0)),
+ # Internal space isn't counted.
+ (' space test', (4, 4)),
+ ('\ttab test', (1, 4)),
+ ('\t\tdouble tabs test', (2, 8)),
+ # Different results when mixing tabs and spaces.
+ (' \tmixed test', (5, 8)),
+ (' \t mixed test', (5, 6)),
+ ('\t mixed test', (5, 8)),
+ # Spaces not divisible by tabwidth.
+ (' \tmixed test', (3, 4)),
+ (' \t mixed test', (3, 5)),
+ ('\t mixed test', (3, 6)),
+ # Only checks spaces and tabs.
+ ('\nnewline test', (0, 0)))
+
+ for line, expected in tests:
+ with self.subTest(line=line):
+ self.assertEqual(
+ editor.get_line_indent(line, tabwidth=4),
+ expected,
+ )
+
+ def test_tabwidth_8(self):
+ # (line, (raw, effective))
+ tests = (('no spaces', (0, 0)),
+ # Internal space isn't counted.
+ (' space test', (8, 8)),
+ ('\ttab test', (1, 8)),
+ ('\t\tdouble tabs test', (2, 16)),
+ # Different results when mixing tabs and spaces.
+ (' \tmixed test', (9, 16)),
+ (' \t mixed test', (9, 10)),
+ ('\t mixed test', (9, 16)),
+ # Spaces not divisible by tabwidth.
+ (' \tmixed test', (3, 8)),
+ (' \t mixed test', (3, 9)),
+ ('\t mixed test', (3, 10)),
+ # Only checks spaces and tabs.
+ ('\nnewline test', (0, 0)))
+
+ for line, expected in tests:
+ with self.subTest(line=line):
+ self.assertEqual(
+ editor.get_line_indent(line, tabwidth=8),
+ expected,
+ )
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_paragraph.py b/Lib/idlelib/idle_test/test_format.py
similarity index 59%
rename from Lib/idlelib/idle_test/test_paragraph.py
rename to Lib/idlelib/idle_test/test_format.py
index 0cb966fb96ca0e..c7b123e9d513af 100644
--- a/Lib/idlelib/idle_test/test_paragraph.py
+++ b/Lib/idlelib/idle_test/test_format.py
@@ -1,10 +1,12 @@
-"Test paragraph, coverage 76%."
+"Test format, coverage 99%."
-from idlelib import paragraph as pg
+from idlelib import format as ft
import unittest
+from unittest import mock
from test.support import requires
from tkinter import Tk, Text
from idlelib.editor import EditorWindow
+from idlelib.idle_test.mock_idle import Editor as MockEditor
class Is_Get_Test(unittest.TestCase):
@@ -16,26 +18,26 @@ class Is_Get_Test(unittest.TestCase):
leadingws_nocomment = ' This is not a comment'
def test_is_all_white(self):
- self.assertTrue(pg.is_all_white(''))
- self.assertTrue(pg.is_all_white('\t\n\r\f\v'))
- self.assertFalse(pg.is_all_white(self.test_comment))
+ self.assertTrue(ft.is_all_white(''))
+ self.assertTrue(ft.is_all_white('\t\n\r\f\v'))
+ self.assertFalse(ft.is_all_white(self.test_comment))
def test_get_indent(self):
Equal = self.assertEqual
- Equal(pg.get_indent(self.test_comment), '')
- Equal(pg.get_indent(self.trailingws_comment), '')
- Equal(pg.get_indent(self.leadingws_comment), ' ')
- Equal(pg.get_indent(self.leadingws_nocomment), ' ')
+ Equal(ft.get_indent(self.test_comment), '')
+ Equal(ft.get_indent(self.trailingws_comment), '')
+ Equal(ft.get_indent(self.leadingws_comment), ' ')
+ Equal(ft.get_indent(self.leadingws_nocomment), ' ')
def test_get_comment_header(self):
Equal = self.assertEqual
# Test comment strings
- Equal(pg.get_comment_header(self.test_comment), '#')
- Equal(pg.get_comment_header(self.trailingws_comment), '#')
- Equal(pg.get_comment_header(self.leadingws_comment), ' #')
+ Equal(ft.get_comment_header(self.test_comment), '#')
+ Equal(ft.get_comment_header(self.trailingws_comment), '#')
+ Equal(ft.get_comment_header(self.leadingws_comment), ' #')
# Test non-comment strings
- Equal(pg.get_comment_header(self.leadingws_nocomment), ' ')
- Equal(pg.get_comment_header(self.test_nocomment), '')
+ Equal(ft.get_comment_header(self.leadingws_nocomment), ' ')
+ Equal(ft.get_comment_header(self.test_nocomment), '')
class FindTest(unittest.TestCase):
@@ -63,7 +65,7 @@ def runcase(self, inserttext, stopline, expected):
linelength = int(text.index("%d.end" % line).split('.')[1])
for col in (0, linelength//2, linelength):
tempindex = "%d.%d" % (line, col)
- self.assertEqual(pg.find_paragraph(text, tempindex), expected)
+ self.assertEqual(ft.find_paragraph(text, tempindex), expected)
text.delete('1.0', 'end')
def test_find_comment(self):
@@ -162,7 +164,7 @@ class ReformatFunctionTest(unittest.TestCase):
def test_reformat_paragraph(self):
Equal = self.assertEqual
- reform = pg.reformat_paragraph
+ reform = ft.reformat_paragraph
hw = "O hello world"
Equal(reform(' ', 1), ' ')
Equal(reform("Hello world", 20), "Hello world")
@@ -193,7 +195,7 @@ def test_reformat_comment(self):
test_string = (
" \"\"\"this is a test of a reformat for a triple quoted string"
" will it reformat to less than 70 characters for me?\"\"\"")
- result = pg.reformat_comment(test_string, 70, " ")
+ result = ft.reformat_comment(test_string, 70, " ")
expected = (
" \"\"\"this is a test of a reformat for a triple quoted string will it\n"
" reformat to less than 70 characters for me?\"\"\"")
@@ -202,7 +204,7 @@ def test_reformat_comment(self):
test_comment = (
"# this is a test of a reformat for a triple quoted string will "
"it reformat to less than 70 characters for me?")
- result = pg.reformat_comment(test_comment, 70, "#")
+ result = ft.reformat_comment(test_comment, 70, "#")
expected = (
"# this is a test of a reformat for a triple quoted string will it\n"
"# reformat to less than 70 characters for me?")
@@ -211,7 +213,7 @@ def test_reformat_comment(self):
class FormatClassTest(unittest.TestCase):
def test_init_close(self):
- instance = pg.FormatParagraph('editor')
+ instance = ft.FormatParagraph('editor')
self.assertEqual(instance.editwin, 'editor')
instance.close()
self.assertEqual(instance.editwin, None)
@@ -273,7 +275,7 @@ def setUpClass(cls):
cls.root.withdraw()
editor = Editor(root=cls.root)
cls.text = editor.text.text # Test code does not need the wrapper.
- cls.formatter = pg.FormatParagraph(editor).format_paragraph_event
+ cls.formatter = ft.FormatParagraph(editor).format_paragraph_event
# Sets the insert mark just after the re-wrapped and inserted text.
@classmethod
@@ -375,5 +377,247 @@ def test_comment_block(self):
## text.delete('1.0', 'end')
+class DummyEditwin:
+ def __init__(self, root, text):
+ self.root = root
+ self.text = text
+ self.indentwidth = 4
+ self.tabwidth = 4
+ self.usetabs = False
+ self.context_use_ps1 = True
+
+ _make_blanks = EditorWindow._make_blanks
+ get_selection_indices = EditorWindow.get_selection_indices
+
+
+class FormatRegionTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ cls.root = Tk()
+ cls.root.withdraw()
+ cls.text = Text(cls.root)
+ cls.text.undo_block_start = mock.Mock()
+ cls.text.undo_block_stop = mock.Mock()
+ cls.editor = DummyEditwin(cls.root, cls.text)
+ cls.formatter = ft.FormatRegion(cls.editor)
+
+ @classmethod
+ def tearDownClass(cls):
+ del cls.text, cls.formatter, cls.editor
+ cls.root.update_idletasks()
+ cls.root.destroy()
+ del cls.root
+
+ def setUp(self):
+ self.text.insert('1.0', self.code_sample)
+
+ def tearDown(self):
+ self.text.delete('1.0', 'end')
+
+ code_sample = """\
+
+class C1():
+ # Class comment.
+ def __init__(self, a, b):
+ self.a = a
+ self.b = b
+
+ def compare(self):
+ if a > b:
+ return a
+ elif a < b:
+ return b
+ else:
+ return None
+"""
+
+ def test_get_region(self):
+ get = self.formatter.get_region
+ text = self.text
+ eq = self.assertEqual
+
+ # Add selection.
+ text.tag_add('sel', '7.0', '10.0')
+ expected_lines = ['',
+ ' def compare(self):',
+ ' if a > b:',
+ '']
+ eq(get(), ('7.0', '10.0', '\n'.join(expected_lines), expected_lines))
+
+ # Remove selection.
+ text.tag_remove('sel', '1.0', 'end')
+ eq(get(), ('15.0', '16.0', '\n', ['', '']))
+
+ def test_set_region(self):
+ set_ = self.formatter.set_region
+ text = self.text
+ eq = self.assertEqual
+
+ save_bell = text.bell
+ text.bell = mock.Mock()
+ line6 = self.code_sample.splitlines()[5]
+ line10 = self.code_sample.splitlines()[9]
+
+ text.tag_add('sel', '6.0', '11.0')
+ head, tail, chars, lines = self.formatter.get_region()
+
+ # No changes.
+ set_(head, tail, chars, lines)
+ text.bell.assert_called_once()
+ eq(text.get('6.0', '11.0'), chars)
+ eq(text.get('sel.first', 'sel.last'), chars)
+ text.tag_remove('sel', '1.0', 'end')
+
+ # Alter selected lines by changing lines and adding a newline.
+ newstring = 'added line 1\n\n\n\n'
+ newlines = newstring.split('\n')
+ set_('7.0', '10.0', chars, newlines)
+ # Selection changed.
+ eq(text.get('sel.first', 'sel.last'), newstring)
+ # Additional line added, so last index is changed.
+ eq(text.get('7.0', '11.0'), newstring)
+ # Before and after lines unchanged.
+ eq(text.get('6.0', '7.0-1c'), line6)
+ eq(text.get('11.0', '12.0-1c'), line10)
+ text.tag_remove('sel', '1.0', 'end')
+
+ text.bell = save_bell
+
+ def test_indent_region_event(self):
+ indent = self.formatter.indent_region_event
+ text = self.text
+ eq = self.assertEqual
+
+ text.tag_add('sel', '7.0', '10.0')
+ indent()
+ # Blank lines aren't affected by indent.
+ eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
+
+ def test_dedent_region_event(self):
+ dedent = self.formatter.dedent_region_event
+ text = self.text
+ eq = self.assertEqual
+
+ text.tag_add('sel', '7.0', '10.0')
+ dedent()
+ # Blank lines aren't affected by dedent.
+ eq(text.get('7.0', '10.0'), ('\ndef compare(self):\n if a > b:\n'))
+
+ def test_comment_region_event(self):
+ comment = self.formatter.comment_region_event
+ text = self.text
+ eq = self.assertEqual
+
+ text.tag_add('sel', '7.0', '10.0')
+ comment()
+ eq(text.get('7.0', '10.0'), ('##\n## def compare(self):\n## if a > b:\n'))
+
+ def test_uncomment_region_event(self):
+ comment = self.formatter.comment_region_event
+ uncomment = self.formatter.uncomment_region_event
+ text = self.text
+ eq = self.assertEqual
+
+ text.tag_add('sel', '7.0', '10.0')
+ comment()
+ uncomment()
+ eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
+
+ # Only remove comments at the beginning of a line.
+ text.tag_remove('sel', '1.0', 'end')
+ text.tag_add('sel', '3.0', '4.0')
+ uncomment()
+ eq(text.get('3.0', '3.end'), (' # Class comment.'))
+
+ self.formatter.set_region('3.0', '4.0', '', ['# Class comment.', ''])
+ uncomment()
+ eq(text.get('3.0', '3.end'), (' Class comment.'))
+
+ @mock.patch.object(ft.FormatRegion, "_asktabwidth")
+ def test_tabify_region_event(self, _asktabwidth):
+ tabify = self.formatter.tabify_region_event
+ text = self.text
+ eq = self.assertEqual
+
+ text.tag_add('sel', '7.0', '10.0')
+ # No tabwidth selected.
+ _asktabwidth.return_value = None
+ self.assertIsNone(tabify())
+
+ _asktabwidth.return_value = 3
+ self.assertIsNotNone(tabify())
+ eq(text.get('7.0', '10.0'), ('\n\t def compare(self):\n\t\t if a > b:\n'))
+
+ @mock.patch.object(ft.FormatRegion, "_asktabwidth")
+ def test_untabify_region_event(self, _asktabwidth):
+ untabify = self.formatter.untabify_region_event
+ text = self.text
+ eq = self.assertEqual
+
+ text.tag_add('sel', '7.0', '10.0')
+ # No tabwidth selected.
+ _asktabwidth.return_value = None
+ self.assertIsNone(untabify())
+
+ _asktabwidth.return_value = 2
+ self.formatter.tabify_region_event()
+ _asktabwidth.return_value = 3
+ self.assertIsNotNone(untabify())
+ eq(text.get('7.0', '10.0'), ('\n def compare(self):\n if a > b:\n'))
+
+ @mock.patch.object(ft, "askinteger")
+ def test_ask_tabwidth(self, askinteger):
+ ask = self.formatter._asktabwidth
+ askinteger.return_value = 10
+ self.assertEqual(ask(), 10)
+
+
+class rstripTest(unittest.TestCase):
+
+ def test_rstrip_line(self):
+ editor = MockEditor()
+ text = editor.text
+ do_rstrip = ft.Rstrip(editor).do_rstrip
+ eq = self.assertEqual
+
+ do_rstrip()
+ eq(text.get('1.0', 'insert'), '')
+ text.insert('1.0', ' ')
+ do_rstrip()
+ eq(text.get('1.0', 'insert'), '')
+ text.insert('1.0', ' \n')
+ do_rstrip()
+ eq(text.get('1.0', 'insert'), '\n')
+
+ def test_rstrip_multiple(self):
+ editor = MockEditor()
+ # Comment above, uncomment 3 below to test with real Editor & Text.
+ #from idlelib.editor import EditorWindow as Editor
+ #from tkinter import Tk
+ #editor = Editor(root=Tk())
+ text = editor.text
+ do_rstrip = ft.Rstrip(editor).do_rstrip
+
+ original = (
+ "Line with an ending tab \n"
+ "Line ending in 5 spaces \n"
+ "Linewithnospaces\n"
+ " indented line\n"
+ " indented line with trailing space \n"
+ " ")
+ stripped = (
+ "Line with an ending tab\n"
+ "Line ending in 5 spaces\n"
+ "Linewithnospaces\n"
+ " indented line\n"
+ " indented line with trailing space\n")
+
+ text.insert('1.0', original)
+ do_rstrip()
+ self.assertEqual(text.get('1.0', 'insert'), stripped)
+
+
if __name__ == '__main__':
unittest.main(verbosity=2, exit=2)
diff --git a/Lib/idlelib/idle_test/test_multicall.py b/Lib/idlelib/idle_test/test_multicall.py
index 68156a743d7b9b..ba582bb3ca51b4 100644
--- a/Lib/idlelib/idle_test/test_multicall.py
+++ b/Lib/idlelib/idle_test/test_multicall.py
@@ -35,6 +35,14 @@ def test_init(self):
mctext = self.mc(self.root)
self.assertIsInstance(mctext._MultiCall__binders, list)
+ def test_yview(self):
+ # Added for tree.wheel_event
+ # (it depends on yview to not be overriden)
+ mc = self.mc
+ self.assertIs(mc.yview, Text.yview)
+ mctext = self.mc(self.root)
+ self.assertIs(mctext.yview.__func__, Text.yview)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_pyparse.py b/Lib/idlelib/idle_test/test_pyparse.py
index 479b84a216b02c..f7154e6ded9574 100644
--- a/Lib/idlelib/idle_test/test_pyparse.py
+++ b/Lib/idlelib/idle_test/test_pyparse.py
@@ -206,8 +206,8 @@ def test_study2(self):
'openbracket', 'bracketing'])
tests = (
TestInfo('', 0, 0, '', None, ((0, 0),)),
- TestInfo("'''This is a multiline continutation docstring.\n\n",
- 0, 49, "'", None, ((0, 0), (0, 1), (49, 0))),
+ TestInfo("'''This is a multiline continuation docstring.\n\n",
+ 0, 48, "'", None, ((0, 0), (0, 1), (48, 0))),
TestInfo(' # Comment\\\n',
0, 12, '', None, ((0, 0), (1, 1), (12, 0))),
# A comment without a space is a special case
diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py
index 581444ca5ef21f..4a096676f25796 100644
--- a/Lib/idlelib/idle_test/test_pyshell.py
+++ b/Lib/idlelib/idle_test/test_pyshell.py
@@ -7,6 +7,28 @@
from tkinter import Tk
+class FunctionTest(unittest.TestCase):
+ # Test stand-alone module level non-gui functions.
+
+ def test_restart_line_wide(self):
+ eq = self.assertEqual
+ for file, mul, extra in (('', 22, ''), ('finame', 21, '=')):
+ width = 60
+ bar = mul * '='
+ with self.subTest(file=file, bar=bar):
+ file = file or 'Shell'
+ line = pyshell.restart_line(width, file)
+ eq(len(line), width)
+ eq(line, f"{bar+extra} RESTART: {file} {bar}")
+
+ def test_restart_line_narrow(self):
+ expect, taglen = "= RESTART: Shell", 16
+ for width in (taglen-1, taglen, taglen+1):
+ with self.subTest(width=width):
+ self.assertEqual(pyshell.restart_line(width, ''), expect)
+ self.assertEqual(pyshell.restart_line(taglen+2, ''), expect+' =')
+
+
class PyShellFileListTest(unittest.TestCase):
@classmethod
diff --git a/Lib/idlelib/idle_test/test_query.py b/Lib/idlelib/idle_test/test_query.py
index c1c4a25cc50608..f957585190dc83 100644
--- a/Lib/idlelib/idle_test/test_query.py
+++ b/Lib/idlelib/idle_test/test_query.py
@@ -1,4 +1,4 @@
-"""Test query, coverage 91%).
+"""Test query, coverage 93%).
Non-gui tests for Query, SectionName, ModuleName, and HelpSource use
dummy versions that extract the non-gui methods and add other needed
@@ -12,7 +12,7 @@
from idlelib import query
import unittest
from test.support import requires
-from tkinter import Tk
+from tkinter import Tk, END
import sys
from unittest import mock
@@ -30,11 +30,9 @@ class Dummy_Query:
ok = query.Query.ok
cancel = query.Query.cancel
# Add attributes and initialization needed for tests.
- entry = Var()
- entry_error = {}
def __init__(self, dummy_entry):
- self.entry.set(dummy_entry)
- self.entry_error['text'] = ''
+ self.entry = Var(value=dummy_entry)
+ self.entry_error = {'text': ''}
self.result = None
self.destroyed = False
def showerror(self, message):
@@ -80,11 +78,9 @@ class SectionNameTest(unittest.TestCase):
class Dummy_SectionName:
entry_ok = query.SectionName.entry_ok # Function being tested.
used_names = ['used']
- entry = Var()
- entry_error = {}
def __init__(self, dummy_entry):
- self.entry.set(dummy_entry)
- self.entry_error['text'] = ''
+ self.entry = Var(value=dummy_entry)
+ self.entry_error = {'text': ''}
def showerror(self, message):
self.entry_error['text'] = message
@@ -115,11 +111,9 @@ class ModuleNameTest(unittest.TestCase):
class Dummy_ModuleName:
entry_ok = query.ModuleName.entry_ok # Function being tested.
text0 = ''
- entry = Var()
- entry_error = {}
def __init__(self, dummy_entry):
- self.entry.set(dummy_entry)
- self.entry_error['text'] = ''
+ self.entry = Var(value=dummy_entry)
+ self.entry_error = {'text': ''}
def showerror(self, message):
self.entry_error['text'] = message
@@ -144,9 +138,7 @@ def test_good_module_name(self):
self.assertEqual(dialog.entry_error['text'], '')
-# 3 HelpSource test classes each test one function.
-
-orig_platform = query.platform
+# 3 HelpSource test classes each test one method.
class HelpsourceBrowsefileTest(unittest.TestCase):
"Test browse_file method of ModuleName subclass of Query."
@@ -178,17 +170,16 @@ class HelpsourcePathokTest(unittest.TestCase):
class Dummy_HelpSource:
path_ok = query.HelpSource.path_ok
- path = Var()
- path_error = {}
def __init__(self, dummy_path):
- self.path.set(dummy_path)
- self.path_error['text'] = ''
+ self.path = Var(value=dummy_path)
+ self.path_error = {'text': ''}
def showerror(self, message, widget=None):
self.path_error['text'] = message
+ orig_platform = query.platform # Set in test_path_ok_file.
@classmethod
def tearDownClass(cls):
- query.platform = orig_platform
+ query.platform = cls.orig_platform
def test_path_ok_blank(self):
dialog = self.Dummy_HelpSource(' ')
@@ -242,6 +233,56 @@ def test_entry_ok_helpsource(self):
self.assertEqual(dialog.entry_ok(), result)
+# 2 CustomRun test classes each test one method.
+
+class CustomRunCLIargsokTest(unittest.TestCase):
+ "Test cli_ok method of the CustomRun subclass of Query."
+
+ class Dummy_CustomRun:
+ cli_args_ok = query.CustomRun.cli_args_ok
+ def __init__(self, dummy_entry):
+ self.entry = Var(value=dummy_entry)
+ self.entry_error = {'text': ''}
+ def showerror(self, message):
+ self.entry_error['text'] = message
+
+ def test_blank_args(self):
+ dialog = self.Dummy_CustomRun(' ')
+ self.assertEqual(dialog.cli_args_ok(), [])
+
+ def test_invalid_args(self):
+ dialog = self.Dummy_CustomRun("'no-closing-quote")
+ self.assertEqual(dialog.cli_args_ok(), None)
+ self.assertIn('No closing', dialog.entry_error['text'])
+
+ def test_good_args(self):
+ args = ['-n', '10', '--verbose', '-p', '/path', '--name']
+ dialog = self.Dummy_CustomRun(' '.join(args) + ' "my name"')
+ self.assertEqual(dialog.cli_args_ok(), args + ["my name"])
+ self.assertEqual(dialog.entry_error['text'], '')
+
+
+class CustomRunEntryokTest(unittest.TestCase):
+ "Test entry_ok method of the CustomRun subclass of Query."
+
+ class Dummy_CustomRun:
+ entry_ok = query.CustomRun.entry_ok
+ entry_error = {}
+ restartvar = Var()
+ def cli_args_ok(self):
+ return self.cli_args
+
+ def test_entry_ok_customrun(self):
+ dialog = self.Dummy_CustomRun()
+ for restart in {True, False}:
+ dialog.restartvar.set(restart)
+ for cli_args, result in ((None, None),
+ (['my arg'], (['my arg'], restart))):
+ with self.subTest(restart=restart, cli_args=cli_args):
+ dialog.cli_args = cli_args
+ self.assertEqual(dialog.entry_ok(), result)
+
+
# GUI TESTS
class QueryGuiTest(unittest.TestCase):
@@ -302,9 +343,7 @@ def test_click_section_name(self):
dialog.entry.insert(0, 'okay')
dialog.button_ok.invoke()
self.assertEqual(dialog.result, 'okay')
- del dialog
root.destroy()
- del root
class ModulenameGuiTest(unittest.TestCase):
@@ -321,9 +360,7 @@ def test_click_module_name(self):
self.assertEqual(dialog.entry.get(), 'idlelib')
dialog.button_ok.invoke()
self.assertTrue(dialog.result.endswith('__init__.py'))
- del dialog
root.destroy()
- del root
class HelpsourceGuiTest(unittest.TestCase):
@@ -343,9 +380,25 @@ def test_click_help_source(self):
dialog.button_ok.invoke()
prefix = "file://" if sys.platform == 'darwin' else ''
Equal(dialog.result, ('__test__', prefix + __file__))
- del dialog
root.destroy()
- del root
+
+
+class CustomRunGuiTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+
+ def test_click_args(self):
+ root = Tk()
+ root.withdraw()
+ dialog = query.CustomRun(root, 'Title',
+ cli_args=['a', 'b=1'], _utest=True)
+ self.assertEqual(dialog.entry.get(), 'a b=1')
+ dialog.entry.insert(END, ' c')
+ dialog.button_ok.invoke()
+ self.assertEqual(dialog.result, (['a', 'b=1', 'c'], True))
+ root.destroy()
if __name__ == '__main__':
diff --git a/Lib/idlelib/idle_test/test_rstrip.py b/Lib/idlelib/idle_test/test_rstrip.py
deleted file mode 100644
index 2bc7c6f035e96b..00000000000000
--- a/Lib/idlelib/idle_test/test_rstrip.py
+++ /dev/null
@@ -1,53 +0,0 @@
-"Test rstrip, coverage 100%."
-
-from idlelib import rstrip
-import unittest
-from idlelib.idle_test.mock_idle import Editor
-
-class rstripTest(unittest.TestCase):
-
- def test_rstrip_line(self):
- editor = Editor()
- text = editor.text
- do_rstrip = rstrip.Rstrip(editor).do_rstrip
-
- do_rstrip()
- self.assertEqual(text.get('1.0', 'insert'), '')
- text.insert('1.0', ' ')
- do_rstrip()
- self.assertEqual(text.get('1.0', 'insert'), '')
- text.insert('1.0', ' \n')
- do_rstrip()
- self.assertEqual(text.get('1.0', 'insert'), '\n')
-
- def test_rstrip_multiple(self):
- editor = Editor()
- # Comment above, uncomment 3 below to test with real Editor & Text.
- #from idlelib.editor import EditorWindow as Editor
- #from tkinter import Tk
- #editor = Editor(root=Tk())
- text = editor.text
- do_rstrip = rstrip.Rstrip(editor).do_rstrip
-
- original = (
- "Line with an ending tab \n"
- "Line ending in 5 spaces \n"
- "Linewithnospaces\n"
- " indented line\n"
- " indented line with trailing space \n"
- " ")
- stripped = (
- "Line with an ending tab\n"
- "Line ending in 5 spaces\n"
- "Linewithnospaces\n"
- " indented line\n"
- " indented line with trailing space\n")
-
- text.insert('1.0', original)
- do_rstrip()
- self.assertEqual(text.get('1.0', 'insert'), stripped)
-
-
-
-if __name__ == '__main__':
- unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_run.py b/Lib/idlelib/idle_test/test_run.py
index 46f0235fbfdca1..cad0b4d98f8e0d 100644
--- a/Lib/idlelib/idle_test/test_run.py
+++ b/Lib/idlelib/idle_test/test_run.py
@@ -6,6 +6,8 @@
from test.support import captured_stderr
import io
+import sys
+
class RunTest(unittest.TestCase):
@@ -260,5 +262,44 @@ def test_close(self):
self.assertRaises(TypeError, f.close, 1)
+class TestSysRecursionLimitWrappers(unittest.TestCase):
+
+ def test_bad_setrecursionlimit_calls(self):
+ run.install_recursionlimit_wrappers()
+ self.addCleanup(run.uninstall_recursionlimit_wrappers)
+ f = sys.setrecursionlimit
+ self.assertRaises(TypeError, f, limit=100)
+ self.assertRaises(TypeError, f, 100, 1000)
+ self.assertRaises(ValueError, f, 0)
+
+ def test_roundtrip(self):
+ run.install_recursionlimit_wrappers()
+ self.addCleanup(run.uninstall_recursionlimit_wrappers)
+
+ # check that setting the recursion limit works
+ orig_reclimit = sys.getrecursionlimit()
+ self.addCleanup(sys.setrecursionlimit, orig_reclimit)
+ sys.setrecursionlimit(orig_reclimit + 3)
+
+ # check that the new limit is returned by sys.getrecursionlimit()
+ new_reclimit = sys.getrecursionlimit()
+ self.assertEqual(new_reclimit, orig_reclimit + 3)
+
+ def test_default_recursion_limit_preserved(self):
+ orig_reclimit = sys.getrecursionlimit()
+ run.install_recursionlimit_wrappers()
+ self.addCleanup(run.uninstall_recursionlimit_wrappers)
+ new_reclimit = sys.getrecursionlimit()
+ self.assertEqual(new_reclimit, orig_reclimit)
+
+ def test_fixdoc(self):
+ def func(): "docstring"
+ run.fixdoc(func, "more")
+ self.assertEqual(func.__doc__, "docstring\n\nmore")
+ func.__doc__ = None
+ run.fixdoc(func, "more")
+ self.assertEqual(func.__doc__, "more")
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_searchbase.py b/Lib/idlelib/idle_test/test_searchbase.py
index 6dd4d79337371d..aee0c4c69929a6 100644
--- a/Lib/idlelib/idle_test/test_searchbase.py
+++ b/Lib/idlelib/idle_test/test_searchbase.py
@@ -4,7 +4,7 @@
import unittest
from test.support import requires
-from tkinter import Tk
+from tkinter import Text, Tk, Toplevel
from tkinter.ttk import Frame
from idlelib import searchengine as se
from idlelib import searchbase as sdb
@@ -47,16 +47,17 @@ def test_open_and_close(self):
# open calls create_widgets, which needs default_command
self.dialog.default_command = None
- # Since text parameter of .open is not used in base class,
- # pass dummy 'text' instead of tk.Text().
- self.dialog.open('text')
+ toplevel = Toplevel(self.root)
+ text = Text(toplevel)
+ self.dialog.open(text)
self.assertEqual(self.dialog.top.state(), 'normal')
self.dialog.close()
self.assertEqual(self.dialog.top.state(), 'withdrawn')
- self.dialog.open('text', searchphrase="hello")
+ self.dialog.open(text, searchphrase="hello")
self.assertEqual(self.dialog.ent.get(), 'hello')
- self.dialog.close()
+ toplevel.update_idletasks()
+ toplevel.destroy()
def test_create_widgets(self):
self.dialog.create_entries = Func()
diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py
new file mode 100644
index 00000000000000..0f5b4c71223240
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_sidebar.py
@@ -0,0 +1,375 @@
+"""Test sidebar, coverage 93%"""
+import idlelib.sidebar
+from sys import platform
+from itertools import chain
+import unittest
+import unittest.mock
+from test.support import requires
+import tkinter as tk
+
+from idlelib.delegator import Delegator
+from idlelib.percolator import Percolator
+
+
+class Dummy_editwin:
+ def __init__(self, text):
+ self.text = text
+ self.text_frame = self.text.master
+ self.per = Percolator(text)
+ self.undo = Delegator()
+ self.per.insertfilter(self.undo)
+
+ def setvar(self, name, value):
+ pass
+
+ def getlineno(self, index):
+ return int(float(self.text.index(index)))
+
+
+class LineNumbersTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ requires('gui')
+ cls.root = tk.Tk()
+
+ cls.text_frame = tk.Frame(cls.root)
+ cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
+ cls.text_frame.rowconfigure(1, weight=1)
+ cls.text_frame.columnconfigure(1, weight=1)
+
+ cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE)
+ cls.text.grid(row=1, column=1, sticky=tk.NSEW)
+
+ cls.editwin = Dummy_editwin(cls.text)
+ cls.editwin.vbar = tk.Scrollbar(cls.text_frame)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.editwin.per.close()
+ cls.root.update()
+ cls.root.destroy()
+ del cls.text, cls.text_frame, cls.editwin, cls.root
+
+ def setUp(self):
+ self.linenumber = idlelib.sidebar.LineNumbers(self.editwin)
+
+ self.highlight_cfg = {"background": '#abcdef',
+ "foreground": '#123456'}
+ orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
+ def mock_idleconf_GetHighlight(theme, element):
+ if element == 'linenumber':
+ return self.highlight_cfg
+ return orig_idleConf_GetHighlight(theme, element)
+ GetHighlight_patcher = unittest.mock.patch.object(
+ idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
+ GetHighlight_patcher.start()
+ self.addCleanup(GetHighlight_patcher.stop)
+
+ self.font_override = 'TkFixedFont'
+ def mock_idleconf_GetFont(root, configType, section):
+ return self.font_override
+ GetFont_patcher = unittest.mock.patch.object(
+ idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
+ GetFont_patcher.start()
+ self.addCleanup(GetFont_patcher.stop)
+
+ def tearDown(self):
+ self.text.delete('1.0', 'end')
+
+ def get_selection(self):
+ return tuple(map(str, self.text.tag_ranges('sel')))
+
+ def get_line_screen_position(self, line):
+ bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c')
+ x = bbox[0] + 2
+ y = bbox[1] + 2
+ return x, y
+
+ def assert_state_disabled(self):
+ state = self.linenumber.sidebar_text.config()['state']
+ self.assertEqual(state[-1], tk.DISABLED)
+
+ def get_sidebar_text_contents(self):
+ return self.linenumber.sidebar_text.get('1.0', tk.END)
+
+ def assert_sidebar_n_lines(self, n_lines):
+ expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), ['']))
+ self.assertEqual(self.get_sidebar_text_contents(), expected)
+
+ def assert_text_equals(self, expected):
+ return self.assertEqual(self.text.get('1.0', 'end'), expected)
+
+ def test_init_empty(self):
+ self.assert_sidebar_n_lines(1)
+
+ def test_init_not_empty(self):
+ self.text.insert('insert', 'foo bar\n'*3)
+ self.assert_text_equals('foo bar\n'*3 + '\n')
+ self.assert_sidebar_n_lines(4)
+
+ def test_toggle_linenumbering(self):
+ self.assertEqual(self.linenumber.is_shown, False)
+ self.linenumber.show_sidebar()
+ self.assertEqual(self.linenumber.is_shown, True)
+ self.linenumber.hide_sidebar()
+ self.assertEqual(self.linenumber.is_shown, False)
+ self.linenumber.hide_sidebar()
+ self.assertEqual(self.linenumber.is_shown, False)
+ self.linenumber.show_sidebar()
+ self.assertEqual(self.linenumber.is_shown, True)
+ self.linenumber.show_sidebar()
+ self.assertEqual(self.linenumber.is_shown, True)
+
+ def test_insert(self):
+ self.text.insert('insert', 'foobar')
+ self.assert_text_equals('foobar\n')
+ self.assert_sidebar_n_lines(1)
+ self.assert_state_disabled()
+
+ self.text.insert('insert', '\nfoo')
+ self.assert_text_equals('foobar\nfoo\n')
+ self.assert_sidebar_n_lines(2)
+ self.assert_state_disabled()
+
+ self.text.insert('insert', 'hello\n'*2)
+ self.assert_text_equals('foobar\nfoohello\nhello\n\n')
+ self.assert_sidebar_n_lines(4)
+ self.assert_state_disabled()
+
+ self.text.insert('insert', '\nworld')
+ self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n')
+ self.assert_sidebar_n_lines(5)
+ self.assert_state_disabled()
+
+ def test_delete(self):
+ self.text.insert('insert', 'foobar')
+ self.assert_text_equals('foobar\n')
+ self.text.delete('1.1', '1.3')
+ self.assert_text_equals('fbar\n')
+ self.assert_sidebar_n_lines(1)
+ self.assert_state_disabled()
+
+ self.text.insert('insert', 'foo\n'*2)
+ self.assert_text_equals('fbarfoo\nfoo\n\n')
+ self.assert_sidebar_n_lines(3)
+ self.assert_state_disabled()
+
+ # Note: deleting up to "2.end" doesn't delete the final newline.
+ self.text.delete('2.0', '2.end')
+ self.assert_text_equals('fbarfoo\n\n\n')
+ self.assert_sidebar_n_lines(3)
+ self.assert_state_disabled()
+
+ self.text.delete('1.3', 'end')
+ self.assert_text_equals('fba\n')
+ self.assert_sidebar_n_lines(1)
+ self.assert_state_disabled()
+
+ # Note: Text widgets always keep a single '\n' character at the end.
+ self.text.delete('1.0', 'end')
+ self.assert_text_equals('\n')
+ self.assert_sidebar_n_lines(1)
+ self.assert_state_disabled()
+
+ def test_sidebar_text_width(self):
+ """
+ Test that linenumber text widget is always at the minimum
+ width
+ """
+ def get_width():
+ return self.linenumber.sidebar_text.config()['width'][-1]
+
+ self.assert_sidebar_n_lines(1)
+ self.assertEqual(get_width(), 1)
+
+ self.text.insert('insert', 'foo')
+ self.assert_sidebar_n_lines(1)
+ self.assertEqual(get_width(), 1)
+
+ self.text.insert('insert', 'foo\n'*8)
+ self.assert_sidebar_n_lines(9)
+ self.assertEqual(get_width(), 1)
+
+ self.text.insert('insert', 'foo\n')
+ self.assert_sidebar_n_lines(10)
+ self.assertEqual(get_width(), 2)
+
+ self.text.insert('insert', 'foo\n')
+ self.assert_sidebar_n_lines(11)
+ self.assertEqual(get_width(), 2)
+
+ self.text.delete('insert -1l linestart', 'insert linestart')
+ self.assert_sidebar_n_lines(10)
+ self.assertEqual(get_width(), 2)
+
+ self.text.delete('insert -1l linestart', 'insert linestart')
+ self.assert_sidebar_n_lines(9)
+ self.assertEqual(get_width(), 1)
+
+ self.text.insert('insert', 'foo\n'*90)
+ self.assert_sidebar_n_lines(99)
+ self.assertEqual(get_width(), 2)
+
+ self.text.insert('insert', 'foo\n')
+ self.assert_sidebar_n_lines(100)
+ self.assertEqual(get_width(), 3)
+
+ self.text.insert('insert', 'foo\n')
+ self.assert_sidebar_n_lines(101)
+ self.assertEqual(get_width(), 3)
+
+ self.text.delete('insert -1l linestart', 'insert linestart')
+ self.assert_sidebar_n_lines(100)
+ self.assertEqual(get_width(), 3)
+
+ self.text.delete('insert -1l linestart', 'insert linestart')
+ self.assert_sidebar_n_lines(99)
+ self.assertEqual(get_width(), 2)
+
+ self.text.delete('50.0 -1c', 'end -1c')
+ self.assert_sidebar_n_lines(49)
+ self.assertEqual(get_width(), 2)
+
+ self.text.delete('5.0 -1c', 'end -1c')
+ self.assert_sidebar_n_lines(4)
+ self.assertEqual(get_width(), 1)
+
+ # Note: Text widgets always keep a single '\n' character at the end.
+ self.text.delete('1.0', 'end -1c')
+ self.assert_sidebar_n_lines(1)
+ self.assertEqual(get_width(), 1)
+
+ def test_click_selection(self):
+ self.linenumber.show_sidebar()
+ self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
+ self.root.update()
+
+ # Click on the second line.
+ x, y = self.get_line_screen_position(2)
+ self.linenumber.sidebar_text.event_generate('', x=x, y=y)
+ self.linenumber.sidebar_text.update()
+ self.root.update()
+
+ self.assertEqual(self.get_selection(), ('2.0', '3.0'))
+
+ def simulate_drag(self, start_line, end_line):
+ start_x, start_y = self.get_line_screen_position(start_line)
+ end_x, end_y = self.get_line_screen_position(end_line)
+
+ self.linenumber.sidebar_text.event_generate('',
+ x=start_x, y=start_y)
+ self.root.update()
+
+ def lerp(a, b, steps):
+ """linearly interpolate from a to b (inclusive) in equal steps"""
+ last_step = steps - 1
+ for i in range(steps):
+ yield ((last_step - i) / last_step) * a + (i / last_step) * b
+
+ for x, y in zip(
+ map(int, lerp(start_x, end_x, steps=11)),
+ map(int, lerp(start_y, end_y, steps=11)),
+ ):
+ self.linenumber.sidebar_text.event_generate('', x=x, y=y)
+ self.root.update()
+
+ self.linenumber.sidebar_text.event_generate('',
+ x=end_x, y=end_y)
+ self.root.update()
+
+ def test_drag_selection_down(self):
+ self.linenumber.show_sidebar()
+ self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
+ self.root.update()
+
+ # Drag from the second line to the fourth line.
+ self.simulate_drag(2, 4)
+ self.assertEqual(self.get_selection(), ('2.0', '5.0'))
+
+ def test_drag_selection_up(self):
+ self.linenumber.show_sidebar()
+ self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
+ self.root.update()
+
+ # Drag from the fourth line to the second line.
+ self.simulate_drag(4, 2)
+ self.assertEqual(self.get_selection(), ('2.0', '5.0'))
+
+ def test_scroll(self):
+ self.linenumber.show_sidebar()
+ self.text.insert('1.0', 'line\n' * 100)
+ self.root.update()
+
+ # Scroll down 10 lines.
+ self.text.yview_scroll(10, 'unit')
+ self.root.update()
+ self.assertEqual(self.text.index('@0,0'), '11.0')
+ self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
+
+ # Generate a mouse-wheel event and make sure it scrolled up or down.
+ # The meaning of the "delta" is OS-dependant, so this just checks for
+ # any change.
+ self.linenumber.sidebar_text.event_generate('',
+ x=0, y=0,
+ delta=10)
+ self.root.update()
+ self.assertNotEqual(self.text.index('@0,0'), '11.0')
+ self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
+
+ def test_font(self):
+ ln = self.linenumber
+
+ orig_font = ln.sidebar_text['font']
+ test_font = 'TkTextFont'
+ self.assertNotEqual(orig_font, test_font)
+
+ # Ensure line numbers aren't shown.
+ ln.hide_sidebar()
+
+ self.font_override = test_font
+ # Nothing breaks when line numbers aren't shown.
+ ln.update_font()
+
+ # Activate line numbers, previous font change is immediately effective.
+ ln.show_sidebar()
+ self.assertEqual(ln.sidebar_text['font'], test_font)
+
+ # Call the font update with line numbers shown, change is picked up.
+ self.font_override = orig_font
+ ln.update_font()
+ self.assertEqual(ln.sidebar_text['font'], orig_font)
+
+ def test_highlight_colors(self):
+ ln = self.linenumber
+
+ orig_colors = dict(self.highlight_cfg)
+ test_colors = {'background': '#222222', 'foreground': '#ffff00'}
+
+ def assert_colors_are_equal(colors):
+ self.assertEqual(ln.sidebar_text['background'], colors['background'])
+ self.assertEqual(ln.sidebar_text['foreground'], colors['foreground'])
+
+ # Ensure line numbers aren't shown.
+ ln.hide_sidebar()
+
+ self.highlight_cfg = test_colors
+ # Nothing breaks with inactive code context.
+ ln.update_colors()
+
+ # Show line numbers, previous colors change is immediately effective.
+ ln.show_sidebar()
+ assert_colors_are_equal(test_colors)
+
+ # Call colors update with no change to the configured colors.
+ ln.update_colors()
+ assert_colors_are_equal(test_colors)
+
+ # Call the colors update with line numbers shown, change is picked up.
+ self.highlight_cfg = orig_colors
+ ln.update_colors()
+ assert_colors_are_equal(orig_colors)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py
index 4e3da030a3adce..1af2ce832845cd 100644
--- a/Lib/idlelib/idle_test/test_squeezer.py
+++ b/Lib/idlelib/idle_test/test_squeezer.py
@@ -82,18 +82,10 @@ def test_several_lines_different_lengths(self):
class SqueezerTest(unittest.TestCase):
"""Tests for the Squeezer class."""
- def tearDown(self):
- # Clean up the Squeezer class's reference to its instance,
- # to avoid side-effects from one test case upon another.
- if Squeezer._instance_weakref is not None:
- Squeezer._instance_weakref = None
-
def make_mock_editor_window(self, with_text_widget=False):
"""Create a mock EditorWindow instance."""
editwin = NonCallableMagicMock()
- # isinstance(editwin, PyShell) must be true for Squeezer to enable
- # auto-squeezing; in practice this will always be true.
- editwin.__class__ = PyShell
+ editwin.width = 80
if with_text_widget:
editwin.root = get_test_tk_root(self)
@@ -107,7 +99,6 @@ def make_squeezer_instance(self, editor_window=None):
if editor_window is None:
editor_window = self.make_mock_editor_window()
squeezer = Squeezer(editor_window)
- squeezer.get_line_width = Mock(return_value=80)
return squeezer
def make_text_widget(self, root=None):
@@ -143,8 +134,8 @@ def test_count_lines(self):
line_width=line_width,
expected=expected):
text = eval(text_code)
- squeezer.get_line_width.return_value = line_width
- self.assertEqual(squeezer.count_lines(text), expected)
+ with patch.object(editwin, 'width', line_width):
+ self.assertEqual(squeezer.count_lines(text), expected)
def test_init(self):
"""Test the creation of Squeezer instances."""
@@ -294,7 +285,6 @@ def test_reload(self):
"""Test the reload() class-method."""
editwin = self.make_mock_editor_window(with_text_widget=True)
squeezer = self.make_squeezer_instance(editwin)
- squeezer.load_font = Mock()
orig_auto_squeeze_min_lines = squeezer.auto_squeeze_min_lines
@@ -307,7 +297,6 @@ def test_reload(self):
Squeezer.reload()
self.assertEqual(squeezer.auto_squeeze_min_lines,
new_auto_squeeze_min_lines)
- squeezer.load_font.assert_called()
def test_reload_no_squeezer_instances(self):
"""Test that Squeezer.reload() runs without any instances existing."""
diff --git a/Lib/idlelib/idle_test/test_textview.py b/Lib/idlelib/idle_test/test_textview.py
index 6f0c1930518a51..7189378ab3dd61 100644
--- a/Lib/idlelib/idle_test/test_textview.py
+++ b/Lib/idlelib/idle_test/test_textview.py
@@ -6,12 +6,12 @@
information about calls.
"""
from idlelib import textview as tv
-import unittest
from test.support import requires
requires('gui')
import os
-from tkinter import Tk
+import unittest
+from tkinter import Tk, TclError, CHAR, NONE, WORD
from tkinter.ttk import Button
from idlelib.idle_test.mock_idle import Func
from idlelib.idle_test.mock_tk import Mbox_func
@@ -69,13 +69,65 @@ def test_ok(self):
view.destroy()
-class TextFrameTest(unittest.TestCase):
+class AutoHideScrollbarTest(unittest.TestCase):
+ # Method set is tested in ScrollableTextFrameTest
+ def test_forbidden_geometry(self):
+ scroll = tv.AutoHideScrollbar(root)
+ self.assertRaises(TclError, scroll.pack)
+ self.assertRaises(TclError, scroll.place)
+
+
+class ScrollableTextFrameTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.root = root = Tk()
+ root.withdraw()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.root.update_idletasks()
+ cls.root.destroy()
+ del cls.root
+
+ def make_frame(self, wrap=NONE, **kwargs):
+ frame = tv.ScrollableTextFrame(self.root, wrap=wrap, **kwargs)
+ def cleanup_frame():
+ frame.update_idletasks()
+ frame.destroy()
+ self.addCleanup(cleanup_frame)
+ return frame
+
+ def test_line1(self):
+ frame = self.make_frame()
+ frame.text.insert('1.0', 'test text')
+ self.assertEqual(frame.text.get('1.0', '1.end'), 'test text')
+
+ def test_horiz_scrollbar(self):
+ # The horizontal scrollbar should be shown/hidden according to
+ # the 'wrap' setting: It should only be shown when 'wrap' is
+ # set to NONE.
+
+ # wrap = NONE -> with horizontal scrolling
+ frame = self.make_frame(wrap=NONE)
+ self.assertEqual(frame.text.cget('wrap'), NONE)
+ self.assertIsNotNone(frame.xscroll)
+
+ # wrap != NONE -> no horizontal scrolling
+ for wrap in [CHAR, WORD]:
+ with self.subTest(wrap=wrap):
+ frame = self.make_frame(wrap=wrap)
+ self.assertEqual(frame.text.cget('wrap'), wrap)
+ self.assertIsNone(frame.xscroll)
+
+
+class ViewFrameTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.root = root = Tk()
root.withdraw()
- cls.frame = tv.TextFrame(root, 'test text')
+ cls.frame = tv.ViewFrame(root, 'test text')
@classmethod
def tearDownClass(cls):
diff --git a/Lib/idlelib/idle_test/test_tooltip.py b/Lib/idlelib/idle_test/test_tooltip.py
index 44ea1110e155dc..c616d4fde3b6d3 100644
--- a/Lib/idlelib/idle_test/test_tooltip.py
+++ b/Lib/idlelib/idle_test/test_tooltip.py
@@ -1,3 +1,10 @@
+"""Test tooltip, coverage 100%.
+
+Coverage is 100% after excluding 6 lines with "# pragma: no cover".
+They involve TclErrors that either should or should not happen in a
+particular situation, and which are 'pass'ed if they do.
+"""
+
from idlelib.tooltip import TooltipBase, Hovertip
from test.support import requires
requires('gui')
@@ -12,16 +19,13 @@ def setUpModule():
global root
root = Tk()
-def root_update():
- global root
- root.update()
-
def tearDownModule():
global root
root.update_idletasks()
root.destroy()
del root
+
def add_call_counting(func):
@wraps(func)
def wrapped_func(*args, **kwargs):
@@ -65,22 +69,25 @@ class HovertipTest(unittest.TestCase):
def setUp(self):
self.top, self.button = _make_top_and_button(self)
+ def is_tipwindow_shown(self, tooltip):
+ return tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()
+
def test_showtip(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ self.assertFalse(self.is_tipwindow_shown(tooltip))
tooltip.showtip()
- self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ self.assertTrue(self.is_tipwindow_shown(tooltip))
def test_showtip_twice(self):
tooltip = Hovertip(self.button, 'ToolTip text')
self.addCleanup(tooltip.hidetip)
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ self.assertFalse(self.is_tipwindow_shown(tooltip))
tooltip.showtip()
- self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ self.assertTrue(self.is_tipwindow_shown(tooltip))
orig_tipwindow = tooltip.tipwindow
tooltip.showtip()
- self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ self.assertTrue(self.is_tipwindow_shown(tooltip))
self.assertIs(tooltip.tipwindow, orig_tipwindow)
def test_hidetip(self):
@@ -88,59 +95,67 @@ def test_hidetip(self):
self.addCleanup(tooltip.hidetip)
tooltip.showtip()
tooltip.hidetip()
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ self.assertFalse(self.is_tipwindow_shown(tooltip))
def test_showtip_on_mouse_enter_no_delay(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
- root_update()
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ root.update()
+ self.assertFalse(self.is_tipwindow_shown(tooltip))
self.button.event_generate('', x=0, y=0)
- root_update()
- self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ root.update()
+ self.assertTrue(self.is_tipwindow_shown(tooltip))
self.assertGreater(len(tooltip.showtip.call_args_list), 0)
- def test_showtip_on_mouse_enter_hover_delay(self):
- tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=50)
- self.addCleanup(tooltip.hidetip)
- tooltip.showtip = add_call_counting(tooltip.showtip)
- root_update()
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ def test_hover_with_delay(self):
+ # Run multiple tests requiring an actual delay simultaneously.
+
+ # Test #1: A hover tip with a non-zero delay appears after the delay.
+ tooltip1 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
+ self.addCleanup(tooltip1.hidetip)
+ tooltip1.showtip = add_call_counting(tooltip1.showtip)
+ root.update()
+ self.assertFalse(self.is_tipwindow_shown(tooltip1))
self.button.event_generate('', x=0, y=0)
- root_update()
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
- time.sleep(0.1)
- root_update()
- self.assertTrue(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
- self.assertGreater(len(tooltip.showtip.call_args_list), 0)
+ root.update()
+ self.assertFalse(self.is_tipwindow_shown(tooltip1))
+
+ # Test #2: A hover tip with a non-zero delay doesn't appear when
+ # the mouse stops hovering over the base widget before the delay
+ # expires.
+ tooltip2 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
+ self.addCleanup(tooltip2.hidetip)
+ tooltip2.showtip = add_call_counting(tooltip2.showtip)
+ root.update()
+ self.button.event_generate('', x=0, y=0)
+ root.update()
+ self.button.event_generate('', x=0, y=0)
+ root.update()
+
+ time.sleep(0.15)
+ root.update()
+
+ # Test #1 assertions.
+ self.assertTrue(self.is_tipwindow_shown(tooltip1))
+ self.assertGreater(len(tooltip1.showtip.call_args_list), 0)
+
+ # Test #2 assertions.
+ self.assertFalse(self.is_tipwindow_shown(tooltip2))
+ self.assertEqual(tooltip2.showtip.call_args_list, [])
def test_hidetip_on_mouse_leave(self):
tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
self.addCleanup(tooltip.hidetip)
tooltip.showtip = add_call_counting(tooltip.showtip)
- root_update()
+ root.update()
self.button.event_generate('', x=0, y=0)
- root_update()
+ root.update()
self.button.event_generate('', x=0, y=0)
- root_update()
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
+ root.update()
+ self.assertFalse(self.is_tipwindow_shown(tooltip))
self.assertGreater(len(tooltip.showtip.call_args_list), 0)
- def test_dont_show_on_mouse_leave_before_delay(self):
- tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=50)
- self.addCleanup(tooltip.hidetip)
- tooltip.showtip = add_call_counting(tooltip.showtip)
- root_update()
- self.button.event_generate('', x=0, y=0)
- root_update()
- self.button.event_generate('', x=0, y=0)
- root_update()
- time.sleep(0.1)
- root_update()
- self.assertFalse(tooltip.tipwindow and tooltip.tipwindow.winfo_viewable())
- self.assertEqual(tooltip.showtip.call_args_list, [])
-
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/idle_test/test_tree.py b/Lib/idlelib/idle_test/test_tree.py
index 9be9abee361f08..b3e4c10cf9e38e 100644
--- a/Lib/idlelib/idle_test/test_tree.py
+++ b/Lib/idlelib/idle_test/test_tree.py
@@ -4,7 +4,7 @@
import unittest
from test.support import requires
requires('gui')
-from tkinter import Tk
+from tkinter import Tk, EventType, SCROLL
class TreeTest(unittest.TestCase):
@@ -29,5 +29,32 @@ def test_init(self):
node.expand()
+class TestScrollEvent(unittest.TestCase):
+
+ def test_wheel_event(self):
+ # Fake widget class containing `yview` only.
+ class _Widget:
+ def __init__(widget, *expected):
+ widget.expected = expected
+ def yview(widget, *args):
+ self.assertTupleEqual(widget.expected, args)
+ # Fake event class
+ class _Event:
+ pass
+ # (type, delta, num, amount)
+ tests = ((EventType.MouseWheel, 120, -1, -5),
+ (EventType.MouseWheel, -120, -1, 5),
+ (EventType.ButtonPress, -1, 4, -5),
+ (EventType.ButtonPress, -1, 5, 5))
+
+ event = _Event()
+ for ty, delta, num, amount in tests:
+ event.type = ty
+ event.delta = delta
+ event.num = num
+ res = tree.wheel_event(event, _Widget(SCROLL, amount, "units"))
+ self.assertEqual(res, "break")
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/Lib/idlelib/mainmenu.py b/Lib/idlelib/mainmenu.py
index f834220fc2bb75..74edce23483829 100644
--- a/Lib/idlelib/mainmenu.py
+++ b/Lib/idlelib/mainmenu.py
@@ -60,6 +60,7 @@
]),
('format', [
+ ('F_ormat Paragraph', '<>'),
('_Indent Region', '<>'),
('_Dedent Region', '<>'),
('Comment _Out Region', '<>'),
@@ -68,14 +69,14 @@
('Untabify Region', '<>'),
('Toggle Tabs', '<>'),
('New Indent Width', '<>'),
- ('F_ormat Paragraph', '<>'),
('S_trip Trailing Whitespace', '<>'),
]),
('run', [
- ('Python Shell', '<>'),
- ('C_heck Module', '<>'),
('R_un Module', '<>'),
+ ('Run... _Customized', '<>'),
+ ('C_heck Module', '<>'),
+ ('Python Shell', '<>'),
]),
('shell', [
@@ -99,7 +100,8 @@
('Configure _IDLE', '<>'),
None,
('Show _Code Context', '<>'),
- ('Zoom Height', '<>'),
+ ('Show _Line Numbers', '<>'),
+ ('_Zoom Height', '<>'),
]),
('window', [
diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py
index ecc53ef0195dc6..90272b6feb4af6 100644
--- a/Lib/idlelib/outwin.py
+++ b/Lib/idlelib/outwin.py
@@ -74,11 +74,11 @@ class OutputWindow(EditorWindow):
("Go to file/line", "<>", None),
]
+ allow_code_context = False
+
def __init__(self, *args):
EditorWindow.__init__(self, *args)
self.text.bind("<>", self.goto_file_line)
- self.text.unbind("<>")
- self.update_menu_state('options', '*Code Context', 'disabled')
# Customize EditorWindow
def ispythonsource(self, filename):
diff --git a/Lib/idlelib/paragraph.py b/Lib/idlelib/paragraph.py
deleted file mode 100644
index 81422571fa32f4..00000000000000
--- a/Lib/idlelib/paragraph.py
+++ /dev/null
@@ -1,194 +0,0 @@
-"""Format a paragraph, comment block, or selection to a max width.
-
-Does basic, standard text formatting, and also understands Python
-comment blocks. Thus, for editing Python source code, this
-extension is really only suitable for reformatting these comment
-blocks or triple-quoted strings.
-
-Known problems with comment reformatting:
-* If there is a selection marked, and the first line of the
- selection is not complete, the block will probably not be detected
- as comments, and will have the normal "text formatting" rules
- applied.
-* If a comment block has leading whitespace that mixes tabs and
- spaces, they will not be considered part of the same block.
-* Fancy comments, like this bulleted list, aren't handled :-)
-"""
-import re
-
-from idlelib.config import idleConf
-
-
-class FormatParagraph:
-
- def __init__(self, editwin):
- self.editwin = editwin
-
- @classmethod
- def reload(cls):
- cls.max_width = idleConf.GetOption('extensions', 'FormatParagraph',
- 'max-width', type='int', default=72)
-
- def close(self):
- self.editwin = None
-
- def format_paragraph_event(self, event, limit=None):
- """Formats paragraph to a max width specified in idleConf.
-
- If text is selected, format_paragraph_event will start breaking lines
- at the max width, starting from the beginning selection.
-
- If no text is selected, format_paragraph_event uses the current
- cursor location to determine the paragraph (lines of text surrounded
- by blank lines) and formats it.
-
- The length limit parameter is for testing with a known value.
- """
- limit = self.max_width if limit is None else limit
- text = self.editwin.text
- first, last = self.editwin.get_selection_indices()
- if first and last:
- data = text.get(first, last)
- comment_header = get_comment_header(data)
- else:
- first, last, comment_header, data = \
- find_paragraph(text, text.index("insert"))
- if comment_header:
- newdata = reformat_comment(data, limit, comment_header)
- else:
- newdata = reformat_paragraph(data, limit)
- text.tag_remove("sel", "1.0", "end")
-
- if newdata != data:
- text.mark_set("insert", first)
- text.undo_block_start()
- text.delete(first, last)
- text.insert(first, newdata)
- text.undo_block_stop()
- else:
- text.mark_set("insert", last)
- text.see("insert")
- return "break"
-
-
-FormatParagraph.reload()
-
-def find_paragraph(text, mark):
- """Returns the start/stop indices enclosing the paragraph that mark is in.
-
- Also returns the comment format string, if any, and paragraph of text
- between the start/stop indices.
- """
- lineno, col = map(int, mark.split("."))
- line = text.get("%d.0" % lineno, "%d.end" % lineno)
-
- # Look for start of next paragraph if the index passed in is a blank line
- while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
- lineno = lineno + 1
- line = text.get("%d.0" % lineno, "%d.end" % lineno)
- first_lineno = lineno
- comment_header = get_comment_header(line)
- comment_header_len = len(comment_header)
-
- # Once start line found, search for end of paragraph (a blank line)
- while get_comment_header(line)==comment_header and \
- not is_all_white(line[comment_header_len:]):
- lineno = lineno + 1
- line = text.get("%d.0" % lineno, "%d.end" % lineno)
- last = "%d.0" % lineno
-
- # Search back to beginning of paragraph (first blank line before)
- lineno = first_lineno - 1
- line = text.get("%d.0" % lineno, "%d.end" % lineno)
- while lineno > 0 and \
- get_comment_header(line)==comment_header and \
- not is_all_white(line[comment_header_len:]):
- lineno = lineno - 1
- line = text.get("%d.0" % lineno, "%d.end" % lineno)
- first = "%d.0" % (lineno+1)
-
- return first, last, comment_header, text.get(first, last)
-
-# This should perhaps be replaced with textwrap.wrap
-def reformat_paragraph(data, limit):
- """Return data reformatted to specified width (limit)."""
- lines = data.split("\n")
- i = 0
- n = len(lines)
- while i < n and is_all_white(lines[i]):
- i = i+1
- if i >= n:
- return data
- indent1 = get_indent(lines[i])
- if i+1 < n and not is_all_white(lines[i+1]):
- indent2 = get_indent(lines[i+1])
- else:
- indent2 = indent1
- new = lines[:i]
- partial = indent1
- while i < n and not is_all_white(lines[i]):
- # XXX Should take double space after period (etc.) into account
- words = re.split(r"(\s+)", lines[i])
- for j in range(0, len(words), 2):
- word = words[j]
- if not word:
- continue # Can happen when line ends in whitespace
- if len((partial + word).expandtabs()) > limit and \
- partial != indent1:
- new.append(partial.rstrip())
- partial = indent2
- partial = partial + word + " "
- if j+1 < len(words) and words[j+1] != " ":
- partial = partial + " "
- i = i+1
- new.append(partial.rstrip())
- # XXX Should reformat remaining paragraphs as well
- new.extend(lines[i:])
- return "\n".join(new)
-
-def reformat_comment(data, limit, comment_header):
- """Return data reformatted to specified width with comment header."""
-
- # Remove header from the comment lines
- lc = len(comment_header)
- data = "\n".join(line[lc:] for line in data.split("\n"))
- # Reformat to maxformatwidth chars or a 20 char width,
- # whichever is greater.
- format_width = max(limit - len(comment_header), 20)
- newdata = reformat_paragraph(data, format_width)
- # re-split and re-insert the comment header.
- newdata = newdata.split("\n")
- # If the block ends in a \n, we don't want the comment prefix
- # inserted after it. (Im not sure it makes sense to reformat a
- # comment block that is not made of complete lines, but whatever!)
- # Can't think of a clean solution, so we hack away
- block_suffix = ""
- if not newdata[-1]:
- block_suffix = "\n"
- newdata = newdata[:-1]
- return '\n'.join(comment_header+line for line in newdata) + block_suffix
-
-def is_all_white(line):
- """Return True if line is empty or all whitespace."""
-
- return re.match(r"^\s*$", line) is not None
-
-def get_indent(line):
- """Return the initial space or tab indent of line."""
- return re.match(r"^([ \t]*)", line).group()
-
-def get_comment_header(line):
- """Return string with leading whitespace and '#' from line or ''.
-
- A null return indicates that the line is not a comment line. A non-
- null return, such as ' #', will be used to find the other lines of
- a comment block with the same indent.
- """
- m = re.match(r"^([ \t]*#*)", line)
- if m is None: return ""
- return m.group(1)
-
-
-if __name__ == "__main__":
- from unittest import main
- main('idlelib.idle_test.test_paragraph', verbosity=2, exit=False)
diff --git a/Lib/idlelib/pyparse.py b/Lib/idlelib/pyparse.py
index 81e7f539803c08..feb57cbb740564 100644
--- a/Lib/idlelib/pyparse.py
+++ b/Lib/idlelib/pyparse.py
@@ -575,7 +575,7 @@ def get_base_indent_string(self):
return code[i:j]
def is_block_opener(self):
- "Return True if the last interesting statemtent opens a block."
+ "Return True if the last interesting statement opens a block."
self._study2()
return self.lastch == ':'
diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py
index 6e0707d68bb6ed..08716a9733b759 100755
--- a/Lib/idlelib/pyshell.py
+++ b/Lib/idlelib/pyshell.py
@@ -387,6 +387,19 @@ def handle_EOF(self):
"Override the base class - just re-raise EOFError"
raise EOFError
+def restart_line(width, filename): # See bpo-38141.
+ """Return width long restart line formatted with filename.
+
+ Fill line with balanced '='s, with any extras and at least one at
+ the beginning. Do not end with a trailing space.
+ """
+ tag = f"= RESTART: {filename or 'Shell'} ="
+ if width >= len(tag):
+ div, mod = divmod((width -len(tag)), 2)
+ return f"{(div+mod)*'='}{tag}{div*'='}"
+ else:
+ return tag[:-2] # Remove ' ='.
+
class ModifiedInterpreter(InteractiveInterpreter):
@@ -394,7 +407,6 @@ def __init__(self, tkconsole):
self.tkconsole = tkconsole
locals = sys.modules['__main__'].__dict__
InteractiveInterpreter.__init__(self, locals=locals)
- self.save_warnings_filters = None
self.restarting = False
self.subprocess_arglist = None
self.port = PORT
@@ -492,9 +504,8 @@ def restart_subprocess(self, with_cwd=False, filename=''):
console.stop_readline()
# annotate restart in shell window and mark it
console.text.delete("iomark", "end-1c")
- tag = 'RESTART: ' + (filename if filename else 'Shell')
- halfbar = ((int(console.width) -len(tag) - 4) // 2) * '='
- console.write("\n{0} {1} {0}".format(halfbar, tag))
+ console.write('\n')
+ console.write(restart_line(console.width, filename))
console.text.mark_set("restart", "end-1c")
console.text.mark_gravity("restart", "left")
if not filename:
@@ -665,8 +676,6 @@ def runsource(self, source):
"Extend base class method: Stuff the source in the line cache first"
filename = self.stuffsource(source)
self.more = 0
- self.save_warnings_filters = warnings.filters[:]
- warnings.filterwarnings(action="error", category=SyntaxWarning)
# at the moment, InteractiveInterpreter expects str
assert isinstance(source, str)
#if isinstance(source, str):
@@ -677,14 +686,9 @@ def runsource(self, source):
# self.tkconsole.resetoutput()
# self.write("Unsupported characters in input\n")
# return
- try:
- # InteractiveInterpreter.runsource() calls its runcode() method,
- # which is overridden (see below)
- return InteractiveInterpreter.runsource(self, source, filename)
- finally:
- if self.save_warnings_filters is not None:
- warnings.filters[:] = self.save_warnings_filters
- self.save_warnings_filters = None
+ # InteractiveInterpreter.runsource() calls its runcode() method,
+ # which is overridden (see below)
+ return InteractiveInterpreter.runsource(self, source, filename)
def stuffsource(self, source):
"Stuff source in the filename cache"
@@ -763,9 +767,6 @@ def runcode(self, code):
if self.tkconsole.executing:
self.interp.restart_subprocess()
self.checklinecache()
- if self.save_warnings_filters is not None:
- warnings.filters[:] = self.save_warnings_filters
- self.save_warnings_filters = None
debugger = self.debugger
try:
self.tkconsole.beginexecuting()
@@ -824,10 +825,10 @@ def display_port_binding_error(self):
def display_no_subprocess_error(self):
tkMessageBox.showerror(
- "Subprocess Startup Error",
- "IDLE's subprocess didn't make connection. Either IDLE can't "
- "start a subprocess or personal firewall software is blocking "
- "the connection.",
+ "Subprocess Connection Error",
+ "IDLE's subprocess didn't make connection.\n"
+ "See the 'Startup failure' section of the IDLE doc, online at\n"
+ "https://docs.python.org/3/library/idle.html#startup-failure",
parent=self.tkconsole.text)
def display_executing_dialog(self):
@@ -861,6 +862,8 @@ class PyShell(OutputWindow):
("Squeeze", "<>"),
]
+ allow_line_numbers = False
+
# New classes
from idlelib.history import History
diff --git a/Lib/idlelib/query.py b/Lib/idlelib/query.py
index f0b72553db87f7..097e6e61e3569c 100644
--- a/Lib/idlelib/query.py
+++ b/Lib/idlelib/query.py
@@ -21,10 +21,11 @@
import importlib
import os
+import shlex
from sys import executable, platform # Platform is set for one test.
-from tkinter import Toplevel, StringVar, W, E, S
-from tkinter.ttk import Frame, Button, Entry, Label
+from tkinter import Toplevel, StringVar, BooleanVar, W, E, S
+from tkinter.ttk import Frame, Button, Entry, Label, Checkbutton
from tkinter import filedialog
from tkinter.font import Font
@@ -35,10 +36,10 @@ class Query(Toplevel):
"""
def __init__(self, parent, title, message, *, text0='', used_names={},
_htest=False, _utest=False):
- """Create popup, do not return until tk widget destroyed.
+ """Create modal popup, return when destroyed.
- Additional subclass init must be done before calling this
- unless _utest=True is passed to suppress wait_window().
+ Additional subclass init must be done before this unless
+ _utest=True is passed to suppress wait_window().
title - string, title of popup dialog
message - string, informational message to display
@@ -47,15 +48,17 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
_htest - bool, change box location when running htest
_utest - bool, leave window hidden and not modal
"""
- Toplevel.__init__(self, parent)
- self.withdraw() # Hide while configuring, especially geometry.
- self.parent = parent
- self.title(title)
+ self.parent = parent # Needed for Font call.
self.message = message
self.text0 = text0
self.used_names = used_names
+
+ Toplevel.__init__(self, parent)
+ self.withdraw() # Hide while configuring, especially geometry.
+ self.title(title)
self.transient(parent)
self.grab_set()
+
windowingsystem = self.tk.call('tk', 'windowingsystem')
if windowingsystem == 'aqua':
try:
@@ -68,9 +71,9 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
self.protocol("WM_DELETE_WINDOW", self.cancel)
self.bind('', self.ok)
self.bind("", self.ok)
- self.resizable(height=False, width=False)
+
self.create_widgets()
- self.update_idletasks() # Needed here for winfo_reqwidth below.
+ self.update_idletasks() # Need here for winfo_reqwidth below.
self.geometry( # Center dialog over parent (or below htest box).
"+%d+%d" % (
parent.winfo_rootx() +
@@ -79,12 +82,19 @@ def __init__(self, parent, title, message, *, text0='', used_names={},
((parent.winfo_height()/2 - self.winfo_reqheight()/2)
if not _htest else 150)
) )
+ self.resizable(height=False, width=False)
+
if not _utest:
self.deiconify() # Unhide now that geometry set.
self.wait_window()
- def create_widgets(self): # Call from override, if any.
- # Bind to self widgets needed for entry_ok or unittest.
+ def create_widgets(self, ok_text='OK'): # Do not replace.
+ """Create entry (rows, extras, buttons.
+
+ Entry stuff on rows 0-2, spanning cols 0-2.
+ Buttons on row 99, cols 1, 2.
+ """
+ # Bind to self the widgets needed for entry_ok or unittest.
self.frame = frame = Frame(self, padding=10)
frame.grid(column=0, row=0, sticky='news')
frame.grid_columnconfigure(0, weight=1)
@@ -98,19 +108,24 @@ def create_widgets(self): # Call from override, if any.
exists=True, root=self.parent)
self.entry_error = Label(frame, text=' ', foreground='red',
font=self.error_font)
- self.button_ok = Button(
- frame, text='OK', default='active', command=self.ok)
- self.button_cancel = Button(
- frame, text='Cancel', command=self.cancel)
-
entrylabel.grid(column=0, row=0, columnspan=3, padx=5, sticky=W)
self.entry.grid(column=0, row=1, columnspan=3, padx=5, sticky=W+E,
pady=[10,0])
self.entry_error.grid(column=0, row=2, columnspan=3, padx=5,
sticky=W+E)
+
+ self.create_extra()
+
+ self.button_ok = Button(
+ frame, text=ok_text, default='active', command=self.ok)
+ self.button_cancel = Button(
+ frame, text='Cancel', command=self.cancel)
+
self.button_ok.grid(column=1, row=99, padx=5)
self.button_cancel.grid(column=2, row=99, padx=5)
+ def create_extra(self): pass # Override to add widgets.
+
def showerror(self, message, widget=None):
#self.bell(displayof=self)
(widget or self.entry_error)['text'] = 'ERROR: ' + message
@@ -226,8 +241,8 @@ def __init__(self, parent, title, *, menuitem='', filepath='',
parent, title, message, text0=menuitem,
used_names=used_names, _htest=_htest, _utest=_utest)
- def create_widgets(self):
- super().create_widgets()
+ def create_extra(self):
+ "Add path widjets to rows 10-12."
frame = self.frame
pathlabel = Label(frame, anchor='w', justify='left',
text='Help File Path: Enter URL or browse for file')
@@ -302,10 +317,60 @@ def entry_ok(self):
path = self.path_ok()
return None if name is None or path is None else (name, path)
+class CustomRun(Query):
+ """Get settings for custom run of module.
+
+ 1. Command line arguments to extend sys.argv.
+ 2. Whether to restart Shell or not.
+ """
+ # Used in runscript.run_custom_event
+
+ def __init__(self, parent, title, *, cli_args=[],
+ _htest=False, _utest=False):
+ """cli_args is a list of strings.
+
+ The list is assigned to the default Entry StringVar.
+ The strings are displayed joined by ' ' for display.
+ """
+ message = 'Command Line Arguments for sys.argv:'
+ super().__init__(
+ parent, title, message, text0=cli_args,
+ _htest=_htest, _utest=_utest)
+
+ def create_extra(self):
+ "Add run mode on rows 10-12."
+ frame = self.frame
+ self.restartvar = BooleanVar(self, value=True)
+ restart = Checkbutton(frame, variable=self.restartvar, onvalue=True,
+ offvalue=False, text='Restart shell')
+ self.args_error = Label(frame, text=' ', foreground='red',
+ font=self.error_font)
+
+ restart.grid(column=0, row=10, columnspan=3, padx=5, sticky='w')
+ self.args_error.grid(column=0, row=12, columnspan=3, padx=5,
+ sticky='we')
+
+ def cli_args_ok(self):
+ "Validity check and parsing for command line arguments."
+ cli_string = self.entry.get().strip()
+ try:
+ cli_args = shlex.split(cli_string, posix=True)
+ except ValueError as err:
+ self.showerror(str(err))
+ return None
+ return cli_args
+
+ def entry_ok(self):
+ "Return apparently valid (cli_args, restart) or None"
+ self.entry_error['text'] = ''
+ cli_args = self.cli_args_ok()
+ restart = self.restartvar.get()
+ return None if cli_args is None else (cli_args, restart)
+
if __name__ == '__main__':
from unittest import main
main('idlelib.idle_test.test_query', verbosity=2, exit=False)
from idlelib.idle_test.htest import run
- run(Query, HelpSource)
+ run(Query, HelpSource, CustomRun)
diff --git a/Lib/idlelib/rstrip.py b/Lib/idlelib/rstrip.py
deleted file mode 100644
index f93b5e8fc20021..00000000000000
--- a/Lib/idlelib/rstrip.py
+++ /dev/null
@@ -1,29 +0,0 @@
-'Provides "Strip trailing whitespace" under the "Format" menu.'
-
-class Rstrip:
-
- def __init__(self, editwin):
- self.editwin = editwin
-
- def do_rstrip(self, event=None):
-
- text = self.editwin.text
- undo = self.editwin.undo
-
- undo.undo_block_start()
-
- end_line = int(float(text.index('end')))
- for cur in range(1, end_line):
- txt = text.get('%i.0' % cur, '%i.end' % cur)
- raw = len(txt)
- cut = len(txt.rstrip())
- # Since text.delete() marks file as changed, even if not,
- # only call it when needed to actually delete something.
- if cut < raw:
- text.delete('%i.%i' % (cur, cut), '%i.end' % cur)
-
- undo.undo_block_stop()
-
-if __name__ == "__main__":
- from unittest import main
- main('idlelib.idle_test.test_rstrip', verbosity=2,)
diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py
index 4075deec51d8ed..41e0ded4402937 100644
--- a/Lib/idlelib/run.py
+++ b/Lib/idlelib/run.py
@@ -4,10 +4,12 @@
f'''{sys.executable} -c "__import__('idlelib.run').run.main()"'''
'.run' is needed because __import__ returns idlelib, not idlelib.run.
"""
+import functools
import io
import linecache
import queue
import sys
+import textwrap
import time
import traceback
import _thread as thread
@@ -199,11 +201,13 @@ def show_socket_error(err, address):
root = tkinter.Tk()
fix_scaling(root)
root.withdraw()
- msg = f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n"\
- f"Fatal OSError #{err.errno}: {err.strerror}.\n"\
- f"See the 'Startup failure' section of the IDLE doc, online at\n"\
- f"https://docs.python.org/3/library/idle.html#startup-failure"
- showerror("IDLE Subprocess Error", msg, parent=root)
+ showerror(
+ "Subprocess Connection Error",
+ f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n"
+ f"Fatal OSError #{err.errno}: {err.strerror}.\n"
+ "See the 'Startup failure' section of the IDLE doc, online at\n"
+ "https://docs.python.org/3/library/idle.html#startup-failure",
+ parent=root)
root.destroy()
def print_exception():
@@ -303,6 +307,67 @@ def fix_scaling(root):
font['size'] = round(-0.75*size)
+def fixdoc(fun, text):
+ tem = (fun.__doc__ + '\n\n') if fun.__doc__ is not None else ''
+ fun.__doc__ = tem + textwrap.fill(textwrap.dedent(text))
+
+RECURSIONLIMIT_DELTA = 30
+
+def install_recursionlimit_wrappers():
+ """Install wrappers to always add 30 to the recursion limit."""
+ # see: bpo-26806
+
+ @functools.wraps(sys.setrecursionlimit)
+ def setrecursionlimit(*args, **kwargs):
+ # mimic the original sys.setrecursionlimit()'s input handling
+ if kwargs:
+ raise TypeError(
+ "setrecursionlimit() takes no keyword arguments")
+ try:
+ limit, = args
+ except ValueError:
+ raise TypeError(f"setrecursionlimit() takes exactly one "
+ f"argument ({len(args)} given)")
+ if not limit > 0:
+ raise ValueError(
+ "recursion limit must be greater or equal than 1")
+
+ return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA)
+
+ fixdoc(setrecursionlimit, f"""\
+ This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible
+ uninterruptible loops.""")
+
+ @functools.wraps(sys.getrecursionlimit)
+ def getrecursionlimit():
+ return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA
+
+ fixdoc(getrecursionlimit, f"""\
+ This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate
+ for the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.""")
+
+ # add the delta to the default recursion limit, to compensate
+ sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA)
+
+ sys.setrecursionlimit = setrecursionlimit
+ sys.getrecursionlimit = getrecursionlimit
+
+
+def uninstall_recursionlimit_wrappers():
+ """Uninstall the recursion limit wrappers from the sys module.
+
+ IDLE only uses this for tests. Users can import run and call
+ this to remove the wrapping.
+ """
+ if (
+ getattr(sys.setrecursionlimit, '__wrapped__', None) and
+ getattr(sys.getrecursionlimit, '__wrapped__', None)
+ ):
+ sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__
+ sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__
+ sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA)
+
+
class MyRPCServer(rpc.RPCServer):
def handle_error(self, request, client_address):
@@ -446,6 +511,8 @@ def handle(self):
# sys.stdin gets changed from within IDLE's shell. See issue17838.
self._keep_stdin = sys.stdin
+ install_recursionlimit_wrappers()
+
self.interp = self.get_remote_proxy("interp")
rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
diff --git a/Lib/idlelib/runscript.py b/Lib/idlelib/runscript.py
index 83433b1cf0a459..f97cf528cce682 100644
--- a/Lib/idlelib/runscript.py
+++ b/Lib/idlelib/runscript.py
@@ -18,6 +18,7 @@
from idlelib.config import idleConf
from idlelib import macosx
from idlelib import pyshell
+from idlelib.query import CustomRun
indent_message = """Error: Inconsistent indentation detected!
@@ -38,6 +39,8 @@ def __init__(self, editwin):
# XXX This should be done differently
self.flist = self.editwin.flist
self.root = self.editwin.root
+ # cli_args is list of strings that extends sys.argv
+ self.cli_args = []
if macosx.isCocoaTk():
self.editwin.text_frame.bind('<>', self._run_module_event)
@@ -108,20 +111,24 @@ def run_module_event(self, event):
# tries to run a module using the keyboard shortcut
# (the menu item works fine).
self.editwin.text_frame.after(200,
- lambda: self.editwin.text_frame.event_generate('<>'))
+ lambda: self.editwin.text_frame.event_generate(
+ '<>'))
return 'break'
else:
return self._run_module_event(event)
- def _run_module_event(self, event):
+ def run_custom_event(self, event):
+ return self._run_module_event(event, customize=True)
+
+ def _run_module_event(self, event, *, customize=False):
"""Run the module after setting up the environment.
- First check the syntax. If OK, make sure the shell is active and
- then transfer the arguments, set the run environment's working
- directory to the directory of the module being executed and also
- add that directory to its sys.path if not already included.
+ First check the syntax. Next get customization. If OK, make
+ sure the shell is active and then transfer the arguments, set
+ the run environment's working directory to the directory of the
+ module being executed and also add that directory to its
+ sys.path if not already included.
"""
-
filename = self.getfilename()
if not filename:
return 'break'
@@ -130,23 +137,35 @@ def _run_module_event(self, event):
return 'break'
if not self.tabnanny(filename):
return 'break'
+ if customize:
+ title = f"Customize {self.editwin.short_title()} Run"
+ run_args = CustomRun(self.shell.text, title,
+ cli_args=self.cli_args).result
+ if not run_args: # User cancelled.
+ return 'break'
+ self.cli_args, restart = run_args if customize else ([], True)
interp = self.shell.interp
- if pyshell.use_subprocess:
- interp.restart_subprocess(with_cwd=False, filename=
- self.editwin._filename_to_unicode(filename))
+ if pyshell.use_subprocess and restart:
+ interp.restart_subprocess(
+ with_cwd=False, filename=
+ self.editwin._filename_to_unicode(filename))
dirname = os.path.dirname(filename)
- # XXX Too often this discards arguments the user just set...
- interp.runcommand("""if 1:
+ argv = [filename]
+ if self.cli_args:
+ argv += self.cli_args
+ interp.runcommand(f"""if 1:
__file__ = {filename!r}
import sys as _sys
from os.path import basename as _basename
+ argv = {argv!r}
if (not _sys.argv or
- _basename(_sys.argv[0]) != _basename(__file__)):
- _sys.argv = [__file__]
+ _basename(_sys.argv[0]) != _basename(__file__) or
+ len(argv) > 1):
+ _sys.argv = argv
import os as _os
_os.chdir({dirname!r})
del _sys, _basename, _os
- \n""".format(filename=filename, dirname=dirname))
+ \n""")
interp.prepend_syspath(filename)
# XXX KBK 03Jul04 When run w/o subprocess, runtime warnings still
# go to __stderr__. With subprocess, they go to the shell.
diff --git a/Lib/idlelib/searchbase.py b/Lib/idlelib/searchbase.py
index 4ed94f186b048d..6fba0b8e583f2b 100644
--- a/Lib/idlelib/searchbase.py
+++ b/Lib/idlelib/searchbase.py
@@ -54,6 +54,7 @@ def open(self, text, searchphrase=None):
else:
self.top.deiconify()
self.top.tkraise()
+ self.top.transient(text.winfo_toplevel())
if searchphrase:
self.ent.delete(0,"end")
self.ent.insert("end",searchphrase)
@@ -66,6 +67,7 @@ def close(self, event=None):
"Put dialog away for later use."
if self.top:
self.top.grab_release()
+ self.top.transient('')
self.top.withdraw()
def create_widgets(self):
diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py
new file mode 100644
index 00000000000000..41c09684a20251
--- /dev/null
+++ b/Lib/idlelib/sidebar.py
@@ -0,0 +1,341 @@
+"""Line numbering implementation for IDLE as an extension.
+Includes BaseSideBar which can be extended for other sidebar based extensions
+"""
+import functools
+import itertools
+
+import tkinter as tk
+from idlelib.config import idleConf
+from idlelib.delegator import Delegator
+
+
+def get_end_linenumber(text):
+ """Utility to get the last line's number in a Tk text widget."""
+ return int(float(text.index('end-1c')))
+
+
+def get_widget_padding(widget):
+ """Get the total padding of a Tk widget, including its border."""
+ # TODO: use also in codecontext.py
+ manager = widget.winfo_manager()
+ if manager == 'pack':
+ info = widget.pack_info()
+ elif manager == 'grid':
+ info = widget.grid_info()
+ else:
+ raise ValueError(f"Unsupported geometry manager: {manager}")
+
+ # All values are passed through getint(), since some
+ # values may be pixel objects, which can't simply be added to ints.
+ padx = sum(map(widget.tk.getint, [
+ info['padx'],
+ widget.cget('padx'),
+ widget.cget('border'),
+ ]))
+ pady = sum(map(widget.tk.getint, [
+ info['pady'],
+ widget.cget('pady'),
+ widget.cget('border'),
+ ]))
+ return padx, pady
+
+
+class BaseSideBar:
+ """
+ The base class for extensions which require a sidebar.
+ """
+ def __init__(self, editwin):
+ self.editwin = editwin
+ self.parent = editwin.text_frame
+ self.text = editwin.text
+
+ _padx, pady = get_widget_padding(self.text)
+ self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE,
+ padx=2, pady=pady,
+ borderwidth=0, highlightthickness=0)
+ self.sidebar_text.config(state=tk.DISABLED)
+ self.text['yscrollcommand'] = self.redirect_yscroll_event
+ self.update_font()
+ self.update_colors()
+
+ self.is_shown = False
+
+ def update_font(self):
+ """Update the sidebar text font, usually after config changes."""
+ font = idleConf.GetFont(self.text, 'main', 'EditorWindow')
+ self._update_font(font)
+
+ def _update_font(self, font):
+ self.sidebar_text['font'] = font
+
+ def update_colors(self):
+ """Update the sidebar text colors, usually after config changes."""
+ colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'normal')
+ self._update_colors(foreground=colors['foreground'],
+ background=colors['background'])
+
+ def _update_colors(self, foreground, background):
+ self.sidebar_text.config(
+ fg=foreground, bg=background,
+ selectforeground=foreground, selectbackground=background,
+ inactiveselectbackground=background,
+ )
+
+ def show_sidebar(self):
+ if not self.is_shown:
+ self.sidebar_text.grid(row=1, column=0, sticky=tk.NSEW)
+ self.is_shown = True
+
+ def hide_sidebar(self):
+ if self.is_shown:
+ self.sidebar_text.grid_forget()
+ self.is_shown = False
+
+ def redirect_yscroll_event(self, *args, **kwargs):
+ """Redirect vertical scrolling to the main editor text widget.
+
+ The scroll bar is also updated.
+ """
+ self.editwin.vbar.set(*args)
+ self.sidebar_text.yview_moveto(args[0])
+ return 'break'
+
+ def redirect_focusin_event(self, event):
+ """Redirect focus-in events to the main editor text widget."""
+ self.text.focus_set()
+ return 'break'
+
+ def redirect_mousebutton_event(self, event, event_name):
+ """Redirect mouse button events to the main editor text widget."""
+ self.text.focus_set()
+ self.text.event_generate(event_name, x=0, y=event.y)
+ return 'break'
+
+ def redirect_mousewheel_event(self, event):
+ """Redirect mouse wheel events to the editwin text widget."""
+ self.text.event_generate('',
+ x=0, y=event.y, delta=event.delta)
+ return 'break'
+
+
+class EndLineDelegator(Delegator):
+ """Generate callbacks with the current end line number after
+ insert or delete operations"""
+ def __init__(self, changed_callback):
+ """
+ changed_callback - Callable, will be called after insert
+ or delete operations with the current
+ end line number.
+ """
+ Delegator.__init__(self)
+ self.changed_callback = changed_callback
+
+ def insert(self, index, chars, tags=None):
+ self.delegate.insert(index, chars, tags)
+ self.changed_callback(get_end_linenumber(self.delegate))
+
+ def delete(self, index1, index2=None):
+ self.delegate.delete(index1, index2)
+ self.changed_callback(get_end_linenumber(self.delegate))
+
+
+class LineNumbers(BaseSideBar):
+ """Line numbers support for editor windows."""
+ def __init__(self, editwin):
+ BaseSideBar.__init__(self, editwin)
+ self.prev_end = 1
+ self._sidebar_width_type = type(self.sidebar_text['width'])
+ self.sidebar_text.config(state=tk.NORMAL)
+ self.sidebar_text.insert('insert', '1', 'linenumber')
+ self.sidebar_text.config(state=tk.DISABLED)
+ self.sidebar_text.config(takefocus=False, exportselection=False)
+ self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT)
+
+ self.bind_events()
+
+ end = get_end_linenumber(self.text)
+ self.update_sidebar_text(end)
+
+ end_line_delegator = EndLineDelegator(self.update_sidebar_text)
+ # Insert the delegator after the undo delegator, so that line numbers
+ # are properly updated after undo and redo actions.
+ end_line_delegator.setdelegate(self.editwin.undo.delegate)
+ self.editwin.undo.setdelegate(end_line_delegator)
+ # Reset the delegator caches of the delegators "above" the
+ # end line delegator we just inserted.
+ delegator = self.editwin.per.top
+ while delegator is not end_line_delegator:
+ delegator.resetcache()
+ delegator = delegator.delegate
+
+ self.is_shown = False
+
+ def bind_events(self):
+ # Ensure focus is always redirected to the main editor text widget.
+ self.sidebar_text.bind('', self.redirect_focusin_event)
+
+ # Redirect mouse scrolling to the main editor text widget.
+ #
+ # Note that without this, scrolling with the mouse only scrolls
+ # the line numbers.
+ self.sidebar_text.bind('', self.redirect_mousewheel_event)
+
+ # Redirect mouse button events to the main editor text widget,
+ # except for the left mouse button (1).
+ #
+ # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel.
+ def bind_mouse_event(event_name, target_event_name):
+ handler = functools.partial(self.redirect_mousebutton_event,
+ event_name=target_event_name)
+ self.sidebar_text.bind(event_name, handler)
+
+ for button in [2, 3, 4, 5]:
+ for event_name in (f'',
+ f'',
+ f'',
+ ):
+ bind_mouse_event(event_name, target_event_name=event_name)
+
+ # Convert double- and triple-click events to normal click events,
+ # since event_generate() doesn't allow generating such events.
+ for event_name in (f'',
+ f'',
+ ):
+ bind_mouse_event(event_name,
+ target_event_name=f'')
+
+ # This is set by b1_mousedown_handler() and read by
+ # drag_update_selection_and_insert_mark(), to know where dragging
+ # began.
+ start_line = None
+ # These are set by b1_motion_handler() and read by selection_handler().
+ # last_y is passed this way since the mouse Y-coordinate is not
+ # available on selection event objects. last_yview is passed this way
+ # to recognize scrolling while the mouse isn't moving.
+ last_y = last_yview = None
+
+ def b1_mousedown_handler(event):
+ # select the entire line
+ lineno = int(float(self.sidebar_text.index(f"@0,{event.y}")))
+ self.text.tag_remove("sel", "1.0", "end")
+ self.text.tag_add("sel", f"{lineno}.0", f"{lineno+1}.0")
+ self.text.mark_set("insert", f"{lineno+1}.0")
+
+ # remember this line in case this is the beginning of dragging
+ nonlocal start_line
+ start_line = lineno
+ self.sidebar_text.bind('', b1_mousedown_handler)
+
+ def b1_mouseup_handler(event):
+ # On mouse up, we're no longer dragging. Set the shared persistent
+ # variables to None to represent this.
+ nonlocal start_line
+ nonlocal last_y
+ nonlocal last_yview
+ start_line = None
+ last_y = None
+ last_yview = None
+ self.sidebar_text.bind('', b1_mouseup_handler)
+
+ def drag_update_selection_and_insert_mark(y_coord):
+ """Helper function for drag and selection event handlers."""
+ lineno = int(float(self.sidebar_text.index(f"@0,{y_coord}")))
+ a, b = sorted([start_line, lineno])
+ self.text.tag_remove("sel", "1.0", "end")
+ self.text.tag_add("sel", f"{a}.0", f"{b+1}.0")
+ self.text.mark_set("insert",
+ f"{lineno if lineno == a else lineno + 1}.0")
+
+ # Special handling of dragging with mouse button 1. In "normal" text
+ # widgets this selects text, but the line numbers text widget has
+ # selection disabled. Still, dragging triggers some selection-related
+ # functionality under the hood. Specifically, dragging to above or
+ # below the text widget triggers scrolling, in a way that bypasses the
+ # other scrolling synchronization mechanisms.i
+ def b1_drag_handler(event, *args):
+ nonlocal last_y
+ nonlocal last_yview
+ last_y = event.y
+ last_yview = self.sidebar_text.yview()
+ if not 0 <= last_y <= self.sidebar_text.winfo_height():
+ self.text.yview_moveto(last_yview[0])
+ drag_update_selection_and_insert_mark(event.y)
+ self.sidebar_text.bind('', b1_drag_handler)
+
+ # With mouse-drag scrolling fixed by the above, there is still an edge-
+ # case we need to handle: When drag-scrolling, scrolling can continue
+ # while the mouse isn't moving, leading to the above fix not scrolling
+ # properly.
+ def selection_handler(event):
+ if last_yview is None:
+ # This logic is only needed while dragging.
+ return
+ yview = self.sidebar_text.yview()
+ if yview != last_yview:
+ self.text.yview_moveto(yview[0])
+ drag_update_selection_and_insert_mark(last_y)
+ self.sidebar_text.bind('<>', selection_handler)
+
+ def update_colors(self):
+ """Update the sidebar text colors, usually after config changes."""
+ colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber')
+ self._update_colors(foreground=colors['foreground'],
+ background=colors['background'])
+
+ def update_sidebar_text(self, end):
+ """
+ Perform the following action:
+ Each line sidebar_text contains the linenumber for that line
+ Synchronize with editwin.text so that both sidebar_text and
+ editwin.text contain the same number of lines"""
+ if end == self.prev_end:
+ return
+
+ width_difference = len(str(end)) - len(str(self.prev_end))
+ if width_difference:
+ cur_width = int(float(self.sidebar_text['width']))
+ new_width = cur_width + width_difference
+ self.sidebar_text['width'] = self._sidebar_width_type(new_width)
+
+ self.sidebar_text.config(state=tk.NORMAL)
+ if end > self.prev_end:
+ new_text = '\n'.join(itertools.chain(
+ [''],
+ map(str, range(self.prev_end + 1, end + 1)),
+ ))
+ self.sidebar_text.insert(f'end -1c', new_text, 'linenumber')
+ else:
+ self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c')
+ self.sidebar_text.config(state=tk.DISABLED)
+
+ self.prev_end = end
+
+
+def _linenumbers_drag_scrolling(parent): # htest #
+ from idlelib.idle_test.test_sidebar import Dummy_editwin
+
+ toplevel = tk.Toplevel(parent)
+ text_frame = tk.Frame(toplevel)
+ text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
+ text_frame.rowconfigure(1, weight=1)
+ text_frame.columnconfigure(1, weight=1)
+
+ font = idleConf.GetFont(toplevel, 'main', 'EditorWindow')
+ text = tk.Text(text_frame, width=80, height=24, wrap=tk.NONE, font=font)
+ text.grid(row=1, column=1, sticky=tk.NSEW)
+
+ editwin = Dummy_editwin(text)
+ editwin.vbar = tk.Scrollbar(text_frame)
+
+ linenumbers = LineNumbers(editwin)
+ linenumbers.show_sidebar()
+
+ text.insert('1.0', '\n'.join('a'*i for i in range(1, 101)))
+
+
+if __name__ == '__main__':
+ from unittest import main
+ main('idlelib.idle_test.test_sidebar', verbosity=2, exit=False)
+
+ from idlelib.idle_test.htest import run
+ run(_linenumbers_drag_scrolling)
diff --git a/Lib/idlelib/squeezer.py b/Lib/idlelib/squeezer.py
index 032401f2abc738..be1538a25fdedf 100644
--- a/Lib/idlelib/squeezer.py
+++ b/Lib/idlelib/squeezer.py
@@ -15,10 +15,8 @@
messages and their tracebacks.
"""
import re
-import weakref
import tkinter as tk
-from tkinter.font import Font
import tkinter.messagebox as tkMessageBox
from idlelib.config import idleConf
@@ -203,8 +201,6 @@ class Squeezer:
This avoids IDLE's shell slowing down considerably, and even becoming
completely unresponsive, when very long outputs are written.
"""
- _instance_weakref = None
-
@classmethod
def reload(cls):
"""Load class variables from config."""
@@ -213,14 +209,6 @@ def reload(cls):
type="int", default=50,
)
- # Loading the font info requires a Tk root. IDLE doesn't rely
- # on Tkinter's "default root", so the instance will reload
- # font info using its editor windows's Tk root.
- if cls._instance_weakref is not None:
- instance = cls._instance_weakref()
- if instance is not None:
- instance.load_font()
-
def __init__(self, editwin):
"""Initialize settings for Squeezer.
@@ -241,9 +229,6 @@ def __init__(self, editwin):
# however, needs to make such changes.
self.base_text = editwin.per.bottom
- Squeezer._instance_weakref = weakref.ref(self)
- self.load_font()
-
# Twice the text widget's border width and internal padding;
# pre-calculated here for the get_line_width() method.
self.window_width_delta = 2 * (
@@ -298,24 +283,7 @@ def count_lines(self, s):
Tabs are considered tabwidth characters long.
"""
- linewidth = self.get_line_width()
- return count_lines_with_wrapping(s, linewidth)
-
- def get_line_width(self):
- # The maximum line length in pixels: The width of the text
- # widget, minus twice the border width and internal padding.
- linewidth_pixels = \
- self.base_text.winfo_width() - self.window_width_delta
-
- # Divide the width of the Text widget by the font width,
- # which is taken to be the width of '0' (zero).
- # http://www.tcl.tk/man/tcl8.6/TkCmd/text.htm#M21
- return linewidth_pixels // self.zero_char_width
-
- def load_font(self):
- text = self.base_text
- self.zero_char_width = \
- Font(text, font=text.cget('font')).measure('0')
+ return count_lines_with_wrapping(s, self.editwin.width)
def squeeze_current_text_event(self, event):
"""squeeze-current-text event handler
diff --git a/Lib/idlelib/textview.py b/Lib/idlelib/textview.py
index 4867a80db1abe6..808a2aefab4f71 100644
--- a/Lib/idlelib/textview.py
+++ b/Lib/idlelib/textview.py
@@ -2,14 +2,15 @@
"""
from tkinter import Toplevel, Text, TclError,\
- HORIZONTAL, VERTICAL, N, S, E, W
+ HORIZONTAL, VERTICAL, NS, EW, NSEW, NONE, WORD, SUNKEN
from tkinter.ttk import Frame, Scrollbar, Button
from tkinter.messagebox import showerror
+from functools import update_wrapper
from idlelib.colorizer import color_config
-class AutoHiddenScrollbar(Scrollbar):
+class AutoHideScrollbar(Scrollbar):
"""A scrollbar that is automatically hidden when not needed.
Only the grid geometry manager is supported.
@@ -28,52 +29,70 @@ def place(self, **kwargs):
raise TclError(f'{self.__class__.__name__} does not support "place"')
-class TextFrame(Frame):
- "Display text with scrollbar."
+class ScrollableTextFrame(Frame):
+ """Display text with scrollbar(s)."""
- def __init__(self, parent, rawtext, wrap='word'):
+ def __init__(self, master, wrap=NONE, **kwargs):
"""Create a frame for Textview.
- parent - parent widget for this frame
- rawtext - text to display
+ master - master widget for this frame
+ wrap - type of text wrapping to use ('word', 'char' or 'none')
+
+ All parameters except for 'wrap' are passed to Frame.__init__().
+
+ The Text widget is accessible via the 'text' attribute.
+
+ Note: Changing the wrapping mode of the text widget after
+ instantiation is not supported.
"""
- super().__init__(parent)
- self['relief'] = 'sunken'
- self['height'] = 700
+ super().__init__(master, **kwargs)
- self.text = text = Text(self, wrap=wrap, highlightthickness=0)
- color_config(text)
- text.grid(row=0, column=0, sticky=N+S+E+W)
+ text = self.text = Text(self, wrap=wrap)
+ text.grid(row=0, column=0, sticky=NSEW)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
- text.insert(0.0, rawtext)
- text['state'] = 'disabled'
- text.focus_set()
# vertical scrollbar
- self.yscroll = yscroll = AutoHiddenScrollbar(self, orient=VERTICAL,
- takefocus=False,
- command=text.yview)
- text['yscrollcommand'] = yscroll.set
- yscroll.grid(row=0, column=1, sticky=N+S)
-
- if wrap == 'none':
- # horizontal scrollbar
- self.xscroll = xscroll = AutoHiddenScrollbar(self, orient=HORIZONTAL,
- takefocus=False,
- command=text.xview)
- text['xscrollcommand'] = xscroll.set
- xscroll.grid(row=1, column=0, sticky=E+W)
+ self.yscroll = AutoHideScrollbar(self, orient=VERTICAL,
+ takefocus=False,
+ command=text.yview)
+ self.yscroll.grid(row=0, column=1, sticky=NS)
+ text['yscrollcommand'] = self.yscroll.set
+
+ # horizontal scrollbar - only when wrap is set to NONE
+ if wrap == NONE:
+ self.xscroll = AutoHideScrollbar(self, orient=HORIZONTAL,
+ takefocus=False,
+ command=text.xview)
+ self.xscroll.grid(row=1, column=0, sticky=EW)
+ text['xscrollcommand'] = self.xscroll.set
+ else:
+ self.xscroll = None
class ViewFrame(Frame):
"Display TextFrame and Close button."
- def __init__(self, parent, text, wrap='word'):
+ def __init__(self, parent, contents, wrap='word'):
+ """Create a frame for viewing text with a "Close" button.
+
+ parent - parent widget for this frame
+ contents - text to display
+ wrap - type of text wrapping to use ('word', 'char' or 'none')
+
+ The Text widget is accessible via the 'text' attribute.
+ """
super().__init__(parent)
self.parent = parent
self.bind('', self.ok)
self.bind('', self.ok)
- self.textframe = TextFrame(self, text, wrap=wrap)
+ self.textframe = ScrollableTextFrame(self, relief=SUNKEN, height=700)
+
+ text = self.text = self.textframe.text
+ text.insert('1.0', contents)
+ text.configure(wrap=wrap, highlightthickness=0, state='disabled')
+ color_config(text)
+ text.focus_set()
+
self.button_ok = button_ok = Button(
self, text='Close', command=self.ok, takefocus=False)
self.textframe.pack(side='top', expand=True, fill='both')
@@ -87,7 +106,7 @@ def ok(self, event=None):
class ViewWindow(Toplevel):
"A simple text viewer dialog for IDLE."
- def __init__(self, parent, title, text, modal=True, wrap='word',
+ def __init__(self, parent, title, contents, modal=True, wrap=WORD,
*, _htest=False, _utest=False):
"""Show the given text in a scrollable window with a 'close' button.
@@ -96,7 +115,7 @@ def __init__(self, parent, title, text, modal=True, wrap='word',
parent - parent of this dialog
title - string which is title of popup dialog
- text - text to display in dialog
+ contents - text to display in dialog
wrap - type of text wrapping to use ('word', 'char' or 'none')
_htest - bool; change box location when running htest.
_utest - bool; don't wait_window when running unittest.
@@ -109,7 +128,7 @@ def __init__(self, parent, title, text, modal=True, wrap='word',
self.geometry(f'=750x500+{x}+{y}')
self.title(title)
- self.viewframe = ViewFrame(self, text, wrap=wrap)
+ self.viewframe = ViewFrame(self, contents, wrap=wrap)
self.protocol("WM_DELETE_WINDOW", self.ok)
self.button_ok = button_ok = Button(self, text='Close',
command=self.ok, takefocus=False)
@@ -129,18 +148,18 @@ def ok(self, event=None):
self.destroy()
-def view_text(parent, title, text, modal=True, wrap='word', _utest=False):
+def view_text(parent, title, contents, modal=True, wrap='word', _utest=False):
"""Create text viewer for given text.
parent - parent of this dialog
title - string which is the title of popup dialog
- text - text to display in this dialog
+ contents - text to display in this dialog
wrap - type of text wrapping to use ('word', 'char' or 'none')
modal - controls if users can interact with other windows while this
dialog is displayed
_utest - bool; controls wait_window on unittest
"""
- return ViewWindow(parent, title, text, modal, wrap=wrap, _utest=_utest)
+ return ViewWindow(parent, title, contents, modal, wrap=wrap, _utest=_utest)
def view_file(parent, title, filename, encoding, modal=True, wrap='word',
diff --git a/Lib/idlelib/tooltip.py b/Lib/idlelib/tooltip.py
index f54ea36f059d6f..69658264dbd4a4 100644
--- a/Lib/idlelib/tooltip.py
+++ b/Lib/idlelib/tooltip.py
@@ -75,7 +75,7 @@ def hidetip(self):
if tw:
try:
tw.destroy()
- except TclError:
+ except TclError: # pragma: no cover
pass
@@ -103,8 +103,8 @@ def __init__(self, anchor_widget, hover_delay=1000):
def __del__(self):
try:
self.anchor_widget.unbind("", self._id1)
- self.anchor_widget.unbind("", self._id2)
- self.anchor_widget.unbind("