From 06e2029dfa500a42e3565ed7ba8573412f153d1c Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Tue, 19 Jun 2018 23:00:35 -0400 Subject: bpo-33907: Rename an IDLE module and class. (GH-7807) Improve consistency and appearance. Module idlelib.calltips is now calltip. Class idlelib.calltip_w.CallTip is now Calltip. --- Lib/idlelib/calltip.py | 178 +++++++++++++++++ Lib/idlelib/calltip_w.py | 12 +- Lib/idlelib/calltips.py | 178 ----------------- Lib/idlelib/editor.py | 12 +- Lib/idlelib/idle_test/test_calltip.py | 218 +++++++++++++++++++++ Lib/idlelib/idle_test/test_calltip_w.py | 2 +- Lib/idlelib/idle_test/test_calltips.py | 218 --------------------- Lib/idlelib/run.py | 4 +- .../IDLE/2018-06-19-22-21-27.bpo-33907.z-_B3N.rst | 3 + 9 files changed, 414 insertions(+), 411 deletions(-) create mode 100644 Lib/idlelib/calltip.py delete mode 100644 Lib/idlelib/calltips.py create mode 100644 Lib/idlelib/idle_test/test_calltip.py delete mode 100644 Lib/idlelib/idle_test/test_calltips.py create mode 100644 Misc/NEWS.d/next/IDLE/2018-06-19-22-21-27.bpo-33907.z-_B3N.rst diff --git a/Lib/idlelib/calltip.py b/Lib/idlelib/calltip.py new file mode 100644 index 0000000..1ff0deb --- /dev/null +++ b/Lib/idlelib/calltip.py @@ -0,0 +1,178 @@ +"""Pop up a reminder of how to call a function. + +Call Tips are floating windows which display function, class, and method +parameter and docstring information when you type an opening parenthesis, and +which disappear when you type a closing parenthesis. +""" +import inspect +import re +import sys +import textwrap +import types + +from idlelib import calltip_w +from idlelib.hyperparser import HyperParser +import __main__ + + +class Calltip: + + def __init__(self, editwin=None): + if editwin is None: # subprocess and test + self.editwin = None + else: + self.editwin = editwin + self.text = editwin.text + self.active_calltip = None + self._calltip_window = self._make_tk_calltip_window + + def close(self): + self._calltip_window = None + + def _make_tk_calltip_window(self): + # See __init__ for usage + return calltip_w.Calltip(self.text) + + def _remove_calltip_window(self, event=None): + if self.active_calltip: + self.active_calltip.hidetip() + self.active_calltip = None + + def force_open_calltip_event(self, event): + "The user selected the menu entry or hotkey, open the tip." + self.open_calltip(True) + return "break" + + def try_open_calltip_event(self, event): + """Happens when it would be nice to open a Calltip, but not really + necessary, for example after an opening bracket, so function calls + won't be made. + """ + self.open_calltip(False) + + def refresh_calltip_event(self, event): + if self.active_calltip and self.active_calltip.is_active(): + self.open_calltip(False) + + def open_calltip(self, evalfuncs): + self._remove_calltip_window() + + hp = HyperParser(self.editwin, "insert") + sur_paren = hp.get_surrounding_brackets('(') + if not sur_paren: + return + hp.set_index(sur_paren[0]) + expression = hp.get_expression() + if not expression: + return + if not evalfuncs and (expression.find('(') != -1): + return + argspec = self.fetch_tip(expression) + if not argspec: + return + self.active_calltip = self._calltip_window() + self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1]) + + def fetch_tip(self, expression): + """Return the argument list and docstring of a function or class. + + If there is a Python subprocess, get the calltip there. Otherwise, + either this fetch_tip() is running in the subprocess or it was + called in an IDLE running without the subprocess. + + The subprocess environment is that of the most recently run script. If + two unrelated modules are being edited some calltips in the current + module may be inoperative if the module was not the last to run. + + To find methods, fetch_tip must be fed a fully qualified name. + + """ + try: + rpcclt = self.editwin.flist.pyshell.interp.rpcclt + except AttributeError: + rpcclt = None + if rpcclt: + return rpcclt.remotecall("exec", "get_the_calltip", + (expression,), {}) + else: + return get_argspec(get_entity(expression)) + + +def get_entity(expression): + """Return the object corresponding to expression evaluated + in a namespace spanning sys.modules and __main.dict__. + """ + if expression: + namespace = sys.modules.copy() + namespace.update(__main__.__dict__) + try: + return eval(expression, namespace) + except BaseException: + # An uncaught exception closes idle, and eval can raise any + # exception, especially if user classes are involved. + return None + +# The following are used in get_argspec and some in tests +_MAX_COLS = 85 +_MAX_LINES = 5 # enough for bytes +_INDENT = ' '*4 # for wrapped signatures +_first_param = re.compile(r'(?<=\()\w*\,?\s*') +_default_callable_argspec = "See source or doc" +_invalid_method = "invalid method signature" +_argument_positional = "\n['/' marks preceding arguments as positional-only]\n" + +def get_argspec(ob): + '''Return a string describing the signature of a callable object, or ''. + + For Python-coded functions and methods, the first line is introspected. + Delete 'self' parameter for classes (.__init__) and bound methods. + The next lines are the first lines of the doc string up to the first + empty line or _MAX_LINES. For builtins, this typically includes + the arguments in addition to the return value. + ''' + argspec = default = "" + try: + ob_call = ob.__call__ + except BaseException: + return default + + fob = ob_call if isinstance(ob_call, types.MethodType) else ob + + try: + argspec = str(inspect.signature(fob)) + except ValueError as err: + msg = str(err) + if msg.startswith(_invalid_method): + return _invalid_method + + if '/' in argspec: + """Using AC's positional argument should add the explain""" + argspec += _argument_positional + if isinstance(fob, type) and argspec == '()': + """fob with no argument, use default callable argspec""" + argspec = _default_callable_argspec + + lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) + if len(argspec) > _MAX_COLS else [argspec] if argspec else []) + + if isinstance(ob_call, types.MethodType): + doc = ob_call.__doc__ + else: + doc = getattr(ob, "__doc__", "") + if doc: + for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: + line = line.strip() + if not line: + break + if len(line) > _MAX_COLS: + line = line[: _MAX_COLS - 3] + '...' + lines.append(line) + argspec = '\n'.join(lines) + if not argspec: + argspec = _default_callable_argspec + return argspec + + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_calltip', verbosity=2) diff --git a/Lib/idlelib/calltip_w.py b/Lib/idlelib/calltip_w.py index 122b73a..b75e4c2 100644 --- a/Lib/idlelib/calltip_w.py +++ b/Lib/idlelib/calltip_w.py @@ -1,7 +1,7 @@ -"""A CallTip window class for Tkinter/IDLE. +"""A Calltip window class for Tkinter/IDLE. After tooltip.py, which uses ideas gleaned from PySol -Used by the calltips IDLE extension. +Used by calltip. """ from tkinter import Toplevel, Label, LEFT, SOLID, TclError @@ -13,7 +13,7 @@ CHECKHIDE_TIME = 100 # milliseconds MARK_RIGHT = "calltipwindowregion_right" -class CallTip: +class Calltip: def __init__(self, widget): self.widget = widget @@ -47,7 +47,7 @@ class CallTip: def showtip(self, text, parenleft, parenright): """Show the calltip, bind events which will close it and reposition it. """ - # Only called in CallTips, where lines are truncated + # Only called in Calltip, where lines are truncated self.text = text if self.tipwindow or not self.text: return @@ -147,7 +147,7 @@ def _calltip_window(parent): # htest # text.pack(side=LEFT, fill=BOTH, expand=1) text.insert("insert", "string.split") top.update() - calltip = CallTip(text) + calltip = Calltip(text) def calltip_show(event): calltip.showtip("(s=Hello world)", "insert", "end") @@ -161,7 +161,7 @@ def _calltip_window(parent): # htest # if __name__ == '__main__': from unittest import main - main('idlelib.idle_test.test_calltips', verbosity=2, exit=False) + main('idlelib.idle_test.test_calltip_w', verbosity=2, exit=False) from idlelib.idle_test.htest import run run(_calltip_window) diff --git a/Lib/idlelib/calltips.py b/Lib/idlelib/calltips.py deleted file mode 100644 index ec8f616..0000000 --- a/Lib/idlelib/calltips.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Pop up a reminder of how to call a function. - -Call Tips are floating windows which display function, class, and method -parameter and docstring information when you type an opening parenthesis, and -which disappear when you type a closing parenthesis. -""" -import inspect -import re -import sys -import textwrap -import types - -from idlelib import calltip_w -from idlelib.hyperparser import HyperParser -import __main__ - - -class CallTips: - - def __init__(self, editwin=None): - if editwin is None: # subprocess and test - self.editwin = None - else: - self.editwin = editwin - self.text = editwin.text - self.active_calltip = None - self._calltip_window = self._make_tk_calltip_window - - def close(self): - self._calltip_window = None - - def _make_tk_calltip_window(self): - # See __init__ for usage - return calltip_w.CallTip(self.text) - - def _remove_calltip_window(self, event=None): - if self.active_calltip: - self.active_calltip.hidetip() - self.active_calltip = None - - def force_open_calltip_event(self, event): - "The user selected the menu entry or hotkey, open the tip." - self.open_calltip(True) - return "break" - - def try_open_calltip_event(self, event): - """Happens when it would be nice to open a CallTip, but not really - necessary, for example after an opening bracket, so function calls - won't be made. - """ - self.open_calltip(False) - - def refresh_calltip_event(self, event): - if self.active_calltip and self.active_calltip.is_active(): - self.open_calltip(False) - - def open_calltip(self, evalfuncs): - self._remove_calltip_window() - - hp = HyperParser(self.editwin, "insert") - sur_paren = hp.get_surrounding_brackets('(') - if not sur_paren: - return - hp.set_index(sur_paren[0]) - expression = hp.get_expression() - if not expression: - return - if not evalfuncs and (expression.find('(') != -1): - return - argspec = self.fetch_tip(expression) - if not argspec: - return - self.active_calltip = self._calltip_window() - self.active_calltip.showtip(argspec, sur_paren[0], sur_paren[1]) - - def fetch_tip(self, expression): - """Return the argument list and docstring of a function or class. - - If there is a Python subprocess, get the calltip there. Otherwise, - either this fetch_tip() is running in the subprocess or it was - called in an IDLE running without the subprocess. - - The subprocess environment is that of the most recently run script. If - two unrelated modules are being edited some calltips in the current - module may be inoperative if the module was not the last to run. - - To find methods, fetch_tip must be fed a fully qualified name. - - """ - try: - rpcclt = self.editwin.flist.pyshell.interp.rpcclt - except AttributeError: - rpcclt = None - if rpcclt: - return rpcclt.remotecall("exec", "get_the_calltip", - (expression,), {}) - else: - return get_argspec(get_entity(expression)) - - -def get_entity(expression): - """Return the object corresponding to expression evaluated - in a namespace spanning sys.modules and __main.dict__. - """ - if expression: - namespace = sys.modules.copy() - namespace.update(__main__.__dict__) - try: - return eval(expression, namespace) - except BaseException: - # An uncaught exception closes idle, and eval can raise any - # exception, especially if user classes are involved. - return None - -# The following are used in get_argspec and some in tests -_MAX_COLS = 85 -_MAX_LINES = 5 # enough for bytes -_INDENT = ' '*4 # for wrapped signatures -_first_param = re.compile(r'(?<=\()\w*\,?\s*') -_default_callable_argspec = "See source or doc" -_invalid_method = "invalid method signature" -_argument_positional = "\n['/' marks preceding arguments as positional-only]\n" - -def get_argspec(ob): - '''Return a string describing the signature of a callable object, or ''. - - For Python-coded functions and methods, the first line is introspected. - Delete 'self' parameter for classes (.__init__) and bound methods. - The next lines are the first lines of the doc string up to the first - empty line or _MAX_LINES. For builtins, this typically includes - the arguments in addition to the return value. - ''' - argspec = default = "" - try: - ob_call = ob.__call__ - except BaseException: - return default - - fob = ob_call if isinstance(ob_call, types.MethodType) else ob - - try: - argspec = str(inspect.signature(fob)) - except ValueError as err: - msg = str(err) - if msg.startswith(_invalid_method): - return _invalid_method - - if '/' in argspec: - """Using AC's positional argument should add the explain""" - argspec += _argument_positional - if isinstance(fob, type) and argspec == '()': - """fob with no argument, use default callable argspec""" - argspec = _default_callable_argspec - - lines = (textwrap.wrap(argspec, _MAX_COLS, subsequent_indent=_INDENT) - if len(argspec) > _MAX_COLS else [argspec] if argspec else []) - - if isinstance(ob_call, types.MethodType): - doc = ob_call.__doc__ - else: - doc = getattr(ob, "__doc__", "") - if doc: - for line in doc.split('\n', _MAX_LINES)[:_MAX_LINES]: - line = line.strip() - if not line: - break - if len(line) > _MAX_COLS: - line = line[: _MAX_COLS - 3] + '...' - lines.append(line) - argspec = '\n'.join(lines) - if not argspec: - argspec = _default_callable_argspec - return argspec - - -if __name__ == '__main__': - from unittest import main - main('idlelib.idle_test.test_calltips', verbosity=2) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 0095bd0..f5a0918 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -54,7 +54,7 @@ class EditorWindow(object): from idlelib.statusbar import MultiStatusBar from idlelib.autocomplete import AutoComplete from idlelib.autoexpand import AutoExpand - from idlelib.calltips import CallTips + from idlelib.calltip import Calltip from idlelib.codecontext import CodeContext from idlelib.paragraph import FormatParagraph from idlelib.parenmatch import ParenMatch @@ -311,11 +311,11 @@ class EditorWindow(object): text.bind("<>", scriptbinding.check_module_event) text.bind("<>", scriptbinding.run_module_event) text.bind("<>", self.RstripExtension(self).do_rstrip) - calltips = self.CallTips(self) - text.bind("<>", calltips.try_open_calltip_event) - #refresh-calltips must come after paren-closed to work right - text.bind("<>", calltips.refresh_calltip_event) - text.bind("<>", calltips.force_open_calltip_event) + ctip = self.Calltip(self) + text.bind("<>", ctip.try_open_calltip_event) + #refresh-calltip must come after paren-closed to work right + text.bind("<>", ctip.refresh_calltip_event) + text.bind("<>", ctip.force_open_calltip_event) text.bind("<>", self.ZoomHeight(self).zoom_height_event) text.bind("<>", self.CodeContext(self).toggle_code_context_event) diff --git a/Lib/idlelib/idle_test/test_calltip.py b/Lib/idlelib/idle_test/test_calltip.py new file mode 100644 index 0000000..0698d4f --- /dev/null +++ b/Lib/idlelib/idle_test/test_calltip.py @@ -0,0 +1,218 @@ +"Test calltip, coverage 60%" + +from idlelib import calltip +import unittest +import textwrap +import types + +default_tip = calltip._default_callable_argspec + + +# Test Class TC is used in multiple get_argspec test methods +class TC(): + 'doc' + tip = "(ai=None, *b)" + def __init__(self, ai=None, *b): 'doc' + __init__.tip = "(self, ai=None, *b)" + def t1(self): 'doc' + t1.tip = "(self)" + def t2(self, ai, b=None): 'doc' + t2.tip = "(self, ai, b=None)" + def t3(self, ai, *args): 'doc' + t3.tip = "(self, ai, *args)" + def t4(self, *args): 'doc' + t4.tip = "(self, *args)" + def t5(self, ai, b=None, *args, **kw): 'doc' + t5.tip = "(self, ai, b=None, *args, **kw)" + def t6(no, self): 'doc' + t6.tip = "(no, self)" + def __call__(self, ci): 'doc' + __call__.tip = "(self, ci)" + # attaching .tip to wrapped methods does not work + @classmethod + def cm(cls, a): 'doc' + @staticmethod + def sm(b): 'doc' + + +tc = TC() +signature = calltip.get_argspec # 2.7 and 3.x use different functions + + +class Get_signatureTest(unittest.TestCase): + # The signature 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, + # but a red buildbot is better than a user crash (as has happened). + # For a simple mismatch, change the expected output to the actual. + + def test_builtins(self): + + # 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__, + '(*args, **kwargs)\n' + 'Create and return a new object. ' + 'See help(type) for accurate signature.') + gtest(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) + + gtest(types.MethodType, "method(function, instance)") + gtest(SB(), default_tip) + import re + p = re.compile('') + gtest(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, '''\ +(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), '''\ +(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_docline_truncation(self): + def f(): pass + f.__doc__ = 'a'*300 + self.assertEqual(signature(f), '()\n' + 'a' * (calltip._MAX_COLS-3) + '...') + + def test_multiline_docstring(self): + # Test fewer lines than max. + self.assertEqual(signature(range), + "range(stop) -> range object\n" + "range(start, stop[, step]) -> range object") + + # Test max lines + self.assertEqual(signature(bytes), '''\ +bytes(iterable_of_ints) -> bytes +bytes(string, encoding[, errors]) -> bytes +bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer +bytes(int) -> bytes object of size given by the parameter initialized with null bytes +bytes() -> empty bytes object''') + + # Test more than max lines + def f(): pass + f.__doc__ = 'a\n' * 15 + self.assertEqual(signature(f), '()' + '\na' * calltip._MAX_LINES) + + def test_functions(self): + def t1(): 'doc' + t1.tip = "()" + def t2(a, b=None): 'doc' + t2.tip = "(a, b=None)" + def t3(a, *args): 'doc' + t3.tip = "(a, *args)" + def t4(*args): 'doc' + t4.tip = "(*args)" + def t5(a, b=None, *args, **kw): 'doc' + t5.tip = "(a, b=None, *args, **kw)" + + 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) + + 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) + + def test_bound_methods(self): + # test that first parameter is correctly removed from argspec + doc = '\ndoc' if TC.__doc__ is not None else '' + 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) + + def test_starred_parameter(self): + # test that starred first parameter is *not* removed from argspec + class C: + def m1(*args): pass + c = C() + for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),): + self.assertEqual(signature(meth), mtip) + + def test_invalid_method_signature(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) + + def test_non_ascii_name(self): + # test that re works to delete a first parameter name that + # includes non-ascii chars, such as various forms of A. + uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)" + 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), "()") + + def test_attribute_exception(self): + class NoCall: + def __getattr__(self, name): + raise BaseException + class CallA(NoCall): + def __call__(oui, a, b, c): + pass + class CallB(NoCall): + def __call__(self, ci): + pass + + for meth, mtip in ((NoCall, default_tip), (CallA, default_tip), + (NoCall(), ''), (CallA(), '(a, b, c)'), + (CallB(), '(ci)')): + self.assertEqual(signature(meth), mtip) + + def test_non_callables(self): + for obj in (0, 0.0, '0', b'0', [], {}): + self.assertEqual(signature(obj), '') + + +class Get_entityTest(unittest.TestCase): + def test_bad_entity(self): + self.assertIsNone(calltip.get_entity('1/0')) + def test_good_entity(self): + self.assertIs(calltip.get_entity('int'), int) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/Lib/idlelib/idle_test/test_calltip_w.py b/Lib/idlelib/idle_test/test_calltip_w.py index 03f1e9a..fc1f111 100644 --- a/Lib/idlelib/idle_test/test_calltip_w.py +++ b/Lib/idlelib/idle_test/test_calltip_w.py @@ -14,7 +14,7 @@ class CallTipTest(unittest.TestCase): cls.root = Tk() cls.root.withdraw() cls.text = Text(cls.root) - cls.calltip = calltip_w.CallTip(cls.text) + cls.calltip = calltip_w.Calltip(cls.text) @classmethod def tearDownClass(cls): diff --git a/Lib/idlelib/idle_test/test_calltips.py b/Lib/idlelib/idle_test/test_calltips.py deleted file mode 100644 index 2cf0e18..0000000 --- a/Lib/idlelib/idle_test/test_calltips.py +++ /dev/null @@ -1,218 +0,0 @@ -"Test calltips, coverage 60%" - -from idlelib import calltips -import unittest -import textwrap -import types - -default_tip = calltips._default_callable_argspec - - -# Test Class TC is used in multiple get_argspec test methods -class TC(): - 'doc' - tip = "(ai=None, *b)" - def __init__(self, ai=None, *b): 'doc' - __init__.tip = "(self, ai=None, *b)" - def t1(self): 'doc' - t1.tip = "(self)" - def t2(self, ai, b=None): 'doc' - t2.tip = "(self, ai, b=None)" - def t3(self, ai, *args): 'doc' - t3.tip = "(self, ai, *args)" - def t4(self, *args): 'doc' - t4.tip = "(self, *args)" - def t5(self, ai, b=None, *args, **kw): 'doc' - t5.tip = "(self, ai, b=None, *args, **kw)" - def t6(no, self): 'doc' - t6.tip = "(no, self)" - def __call__(self, ci): 'doc' - __call__.tip = "(self, ci)" - # attaching .tip to wrapped methods does not work - @classmethod - def cm(cls, a): 'doc' - @staticmethod - def sm(b): 'doc' - - -tc = TC() -signature = calltips.get_argspec # 2.7 and 3.x use different functions - - -class Get_signatureTest(unittest.TestCase): - # The signature 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, - # but a red buildbot is better than a user crash (as has happened). - # For a simple mismatch, change the expected output to the actual. - - def test_builtins(self): - - # 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=(), /)' + calltips._argument_positional - + '\n' + List.__doc__) - gtest(list.__new__, - '(*args, **kwargs)\n' - 'Create and return a new object. ' - 'See help(type) for accurate signature.') - gtest(list.__init__, - '(self, /, *args, **kwargs)' - + calltips._argument_positional + '\n' + - 'Initialize self. See help(type(self)) for accurate signature.') - append_doc = (calltips._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) - - gtest(types.MethodType, "method(function, instance)") - gtest(SB(), default_tip) - import re - p = re.compile('') - gtest(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, '''\ -(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), '''\ -(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_docline_truncation(self): - def f(): pass - f.__doc__ = 'a'*300 - self.assertEqual(signature(f), '()\n' + 'a' * (calltips._MAX_COLS-3) + '...') - - def test_multiline_docstring(self): - # Test fewer lines than max. - self.assertEqual(signature(range), - "range(stop) -> range object\n" - "range(start, stop[, step]) -> range object") - - # Test max lines - self.assertEqual(signature(bytes), '''\ -bytes(iterable_of_ints) -> bytes -bytes(string, encoding[, errors]) -> bytes -bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer -bytes(int) -> bytes object of size given by the parameter initialized with null bytes -bytes() -> empty bytes object''') - - # Test more than max lines - def f(): pass - f.__doc__ = 'a\n' * 15 - self.assertEqual(signature(f), '()' + '\na' * calltips._MAX_LINES) - - def test_functions(self): - def t1(): 'doc' - t1.tip = "()" - def t2(a, b=None): 'doc' - t2.tip = "(a, b=None)" - def t3(a, *args): 'doc' - t3.tip = "(a, *args)" - def t4(*args): 'doc' - t4.tip = "(*args)" - def t5(a, b=None, *args, **kw): 'doc' - t5.tip = "(a, b=None, *args, **kw)" - - 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) - - 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) - - def test_bound_methods(self): - # test that first parameter is correctly removed from argspec - doc = '\ndoc' if TC.__doc__ is not None else '' - 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) - - def test_starred_parameter(self): - # test that starred first parameter is *not* removed from argspec - class C: - def m1(*args): pass - c = C() - for meth, mtip in ((C.m1, '(*args)'), (c.m1, "(*args)"),): - self.assertEqual(signature(meth), mtip) - - def test_invalid_method_signature(self): - class C: - def m2(**kwargs): pass - class Test: - def __call__(*, a): pass - - mtip = calltips._invalid_method - self.assertEqual(signature(C().m2), mtip) - self.assertEqual(signature(Test()), mtip) - - def test_non_ascii_name(self): - # test that re works to delete a first parameter name that - # includes non-ascii chars, such as various forms of A. - uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)" - assert calltips._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), "()") - - def test_attribute_exception(self): - class NoCall: - def __getattr__(self, name): - raise BaseException - class CallA(NoCall): - def __call__(oui, a, b, c): - pass - class CallB(NoCall): - def __call__(self, ci): - pass - - for meth, mtip in ((NoCall, default_tip), (CallA, default_tip), - (NoCall(), ''), (CallA(), '(a, b, c)'), - (CallB(), '(ci)')): - self.assertEqual(signature(meth), mtip) - - def test_non_callables(self): - for obj in (0, 0.0, '0', b'0', [], {}): - self.assertEqual(signature(obj), '') - - -class Get_entityTest(unittest.TestCase): - def test_bad_entity(self): - self.assertIsNone(calltips.get_entity('1/0')) - def test_good_entity(self): - self.assertIs(calltips.get_entity('int'), int) - - -if __name__ == '__main__': - unittest.main(verbosity=2) diff --git a/Lib/idlelib/run.py b/Lib/idlelib/run.py index 176fe3d..6fa373f 100644 --- a/Lib/idlelib/run.py +++ b/Lib/idlelib/run.py @@ -11,7 +11,7 @@ import warnings import tkinter # Tcl, deletions, messagebox if startup fails from idlelib import autocomplete # AutoComplete, fetch_encodings -from idlelib import calltips # CallTips +from idlelib import calltip # Calltip from idlelib import debugger_r # start_debugger from idlelib import debugobj_r # remote_object_tree_item from idlelib import iomenu # encoding @@ -462,7 +462,7 @@ class Executive(object): def __init__(self, rpchandler): self.rpchandler = rpchandler self.locals = __main__.__dict__ - self.calltip = calltips.CallTips() + self.calltip = calltip.Calltip() self.autocomplete = autocomplete.AutoComplete() def runcode(self, code): diff --git a/Misc/NEWS.d/next/IDLE/2018-06-19-22-21-27.bpo-33907.z-_B3N.rst b/Misc/NEWS.d/next/IDLE/2018-06-19-22-21-27.bpo-33907.z-_B3N.rst new file mode 100644 index 0000000..9c5a070 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2018-06-19-22-21-27.bpo-33907.z-_B3N.rst @@ -0,0 +1,3 @@ +For consistency and appearance, rename an IDLE module and class. Module +idlelib.calltips is now calltip. Class idlelib.calltip_w.CallTip is now +Calltip. -- cgit v0.12