1"Test calltip, coverage 76%"
2
3from idlelib import calltip
4import unittest
5from unittest.mock import Mock
6import textwrap
7import types
8import re
9from idlelib.idle_test.mock_tk import Text
10
11
12# Test Class TC is used in multiple get_argspec test methods
13class TC:
14    'doc'
15    tip = "(ai=None, *b)"
16    def __init__(self, ai=None, *b): 'doc'
17    __init__.tip = "(self, ai=None, *b)"
18    def t1(self): 'doc'
19    t1.tip = "(self)"
20    def t2(self, ai, b=None): 'doc'
21    t2.tip = "(self, ai, b=None)"
22    def t3(self, ai, *args): 'doc'
23    t3.tip = "(self, ai, *args)"
24    def t4(self, *args): 'doc'
25    t4.tip = "(self, *args)"
26    def t5(self, ai, b=None, *args, **kw): 'doc'
27    t5.tip = "(self, ai, b=None, *args, **kw)"
28    def t6(no, self): 'doc'
29    t6.tip = "(no, self)"
30    def __call__(self, ci): 'doc'
31    __call__.tip = "(self, ci)"
32    def nd(self): pass  # No doc.
33    # attaching .tip to wrapped methods does not work
34    @classmethod
35    def cm(cls, a): 'doc'
36    @staticmethod
37    def sm(b): 'doc'
38
39
40tc = TC()
41default_tip = calltip._default_callable_argspec
42get_spec = calltip.get_argspec
43
44
45class Get_argspecTest(unittest.TestCase):
46    # The get_spec function must return a string, even if blank.
47    # Test a variety of objects to be sure that none cause it to raise
48    # (quite aside from getting as correct an answer as possible).
49    # The tests of builtins may break if inspect or the docstrings change,
50    # but a red buildbot is better than a user crash (as has happened).
51    # For a simple mismatch, change the expected output to the actual.
52
53    def test_builtins(self):
54
55        def tiptest(obj, out):
56            self.assertEqual(get_spec(obj), out)
57
58        # Python class that inherits builtin methods
59        class List(list): "List() doc"
60
61        # Simulate builtin with no docstring for default tip test
62        class SB:  __call__ = None
63
64        if List.__doc__ is not None:
65            tiptest(List,
66                    f'(iterable=(), /)'
67                    f'\n{List.__doc__}')
68        tiptest(list.__new__,
69              '(*args, **kwargs)\n'
70              'Create and return a new object.  '
71              'See help(type) for accurate signature.')
72        tiptest(list.__init__,
73              '(self, /, *args, **kwargs)\n'
74              'Initialize self.  See help(type(self)) for accurate signature.')
75        append_doc = "\nAppend object to the end of the list."
76        tiptest(list.append, '(self, object, /)' + append_doc)
77        tiptest(List.append, '(self, object, /)' + append_doc)
78        tiptest([].append, '(object, /)' + append_doc)
79
80        tiptest(types.MethodType,
81              '(function, instance, /)\n'
82              'Create a bound instance method object.')
83        tiptest(SB(), default_tip)
84
85        p = re.compile('')
86        tiptest(re.sub, '''\
87(pattern, repl, string, count=0, flags=0)
88Return the string obtained by replacing the leftmost
89non-overlapping occurrences of the pattern in string by the
90replacement repl.  repl can be either a string or a callable;
91if a string, backslash escapes in it are processed.  If it is
92a callable, it's passed the Match object and must return''')
93        tiptest(p.sub, '''\
94(repl, string, count=0)
95Return the string obtained by replacing the leftmost \
96non-overlapping occurrences o...''')
97
98    def test_signature_wrap(self):
99        if textwrap.TextWrapper.__doc__ is not None:
100            self.assertEqual(get_spec(textwrap.TextWrapper), '''\
101(width=70, initial_indent='', subsequent_indent='', expand_tabs=True,
102    replace_whitespace=True, fix_sentence_endings=False, break_long_words=True,
103    drop_whitespace=True, break_on_hyphens=True, tabsize=8, *, max_lines=None,
104    placeholder=' [...]')
105Object for wrapping/filling text.  The public interface consists of
106the wrap() and fill() methods; the other methods are just there for
107subclasses to override in order to tweak the default behaviour.
108If you want to completely replace the main wrapping algorithm,
109you\'ll probably have to override _wrap_chunks().''')
110
111    def test_properly_formatted(self):
112
113        def foo(s='a'*100):
114            pass
115
116        def bar(s='a'*100):
117            """Hello Guido"""
118            pass
119
120        def baz(s='a'*100, z='b'*100):
121            pass
122
123        indent = calltip._INDENT
124
125        sfoo = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
126               "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
127               "aaaaaaaaaa')"
128        sbar = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
129               "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
130               "aaaaaaaaaa')\nHello Guido"
131        sbaz = "(s='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\
132               "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + indent + "aaaaaaaaa"\
133               "aaaaaaaaaa', z='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\
134               "bbbbbbbbbbbbbbbbb\n" + indent + "bbbbbbbbbbbbbbbbbbbbbb"\
135               "bbbbbbbbbbbbbbbbbbbbbb')"
136
137        for func,doc in [(foo, sfoo), (bar, sbar), (baz, sbaz)]:
138            with self.subTest(func=func, doc=doc):
139                self.assertEqual(get_spec(func), doc)
140
141    def test_docline_truncation(self):
142        def f(): pass
143        f.__doc__ = 'a'*300
144        self.assertEqual(get_spec(f), f"()\n{'a'*(calltip._MAX_COLS-3) + '...'}")
145
146    def test_multiline_docstring(self):
147        # Test fewer lines than max.
148        self.assertEqual(get_spec(range),
149                "range(stop) -> range object\n"
150                "range(start, stop[, step]) -> range object")
151
152        # Test max lines
153        self.assertEqual(get_spec(bytes), '''\
154bytes(iterable_of_ints) -> bytes
155bytes(string, encoding[, errors]) -> bytes
156bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer
157bytes(int) -> bytes object of size given by the parameter initialized with null bytes
158bytes() -> empty bytes object''')
159
160        # Test more than max lines
161        def f(): pass
162        f.__doc__ = 'a\n' * 15
163        self.assertEqual(get_spec(f), '()' + '\na' * calltip._MAX_LINES)
164
165    def test_functions(self):
166        def t1(): 'doc'
167        t1.tip = "()"
168        def t2(a, b=None): 'doc'
169        t2.tip = "(a, b=None)"
170        def t3(a, *args): 'doc'
171        t3.tip = "(a, *args)"
172        def t4(*args): 'doc'
173        t4.tip = "(*args)"
174        def t5(a, b=None, *args, **kw): 'doc'
175        t5.tip = "(a, b=None, *args, **kw)"
176
177        doc = '\ndoc' if t1.__doc__ is not None else ''
178        for func in (t1, t2, t3, t4, t5, TC):
179            with self.subTest(func=func):
180                self.assertEqual(get_spec(func), func.tip + doc)
181
182    def test_methods(self):
183        doc = '\ndoc' if TC.__doc__ is not None else ''
184        for meth in (TC.t1, TC.t2, TC.t3, TC.t4, TC.t5, TC.t6, TC.__call__):
185            with self.subTest(meth=meth):
186                self.assertEqual(get_spec(meth), meth.tip + doc)
187        self.assertEqual(get_spec(TC.cm), "(a)" + doc)
188        self.assertEqual(get_spec(TC.sm), "(b)" + doc)
189
190    def test_bound_methods(self):
191        # test that first parameter is correctly removed from argspec
192        doc = '\ndoc' if TC.__doc__ is not None else ''
193        for meth, mtip  in ((tc.t1, "()"), (tc.t4, "(*args)"),
194                            (tc.t6, "(self)"), (tc.__call__, '(ci)'),
195                            (tc, '(ci)'), (TC.cm, "(a)"),):
196            with self.subTest(meth=meth, mtip=mtip):
197                self.assertEqual(get_spec(meth), mtip + doc)
198
199    def test_starred_parameter(self):
200        # test that starred first parameter is *not* removed from argspec
201        class C:
202            def m1(*args): pass
203        c = C()
204        for meth, mtip  in ((C.m1, '(*args)'), (c.m1, "(*args)"),):
205            with self.subTest(meth=meth, mtip=mtip):
206                self.assertEqual(get_spec(meth), mtip)
207
208    def test_invalid_method_get_spec(self):
209        class C:
210            def m2(**kwargs): pass
211        class Test:
212            def __call__(*, a): pass
213
214        mtip = calltip._invalid_method
215        self.assertEqual(get_spec(C().m2), mtip)
216        self.assertEqual(get_spec(Test()), mtip)
217
218    def test_non_ascii_name(self):
219        # test that re works to delete a first parameter name that
220        # includes non-ascii chars, such as various forms of A.
221        uni = "(A\u0391\u0410\u05d0\u0627\u0905\u1e00\u3042, a)"
222        assert calltip._first_param.sub('', uni) == '(a)'
223
224    def test_no_docstring(self):
225        for meth, mtip in ((TC.nd, "(self)"), (tc.nd, "()")):
226            with self.subTest(meth=meth, mtip=mtip):
227                self.assertEqual(get_spec(meth), mtip)
228
229    def test_buggy_getattr_class(self):
230        class NoCall:
231            def __getattr__(self, name):  # Not invoked for class attribute.
232                raise IndexError  # Bug.
233        class CallA(NoCall):
234            def __call__(self, ci):  # Bug does not matter.
235                pass
236        class CallB(NoCall):
237            def __call__(oui, a, b, c):  # Non-standard 'self'.
238                pass
239
240        for meth, mtip  in ((NoCall, default_tip), (CallA, default_tip),
241                            (NoCall(), ''), (CallA(), '(ci)'),
242                            (CallB(), '(a, b, c)')):
243            with self.subTest(meth=meth, mtip=mtip):
244                self.assertEqual(get_spec(meth), mtip)
245
246    def test_metaclass_class(self):  # Failure case for issue 38689.
247        class Type(type):  # Type() requires 3 type args, returns class.
248            __class__ = property({}.__getitem__, {}.__setitem__)
249        class Object(metaclass=Type):
250            __slots__ = '__class__'
251        for meth, mtip  in ((Type, get_spec(type)), (Object, default_tip),
252                            (Object(), '')):
253            with self.subTest(meth=meth, mtip=mtip):
254                self.assertEqual(get_spec(meth), mtip)
255
256    def test_non_callables(self):
257        for obj in (0, 0.0, '0', b'0', [], {}):
258            with self.subTest(obj=obj):
259                self.assertEqual(get_spec(obj), '')
260
261
262class Get_entityTest(unittest.TestCase):
263    def test_bad_entity(self):
264        self.assertIsNone(calltip.get_entity('1/0'))
265    def test_good_entity(self):
266        self.assertIs(calltip.get_entity('int'), int)
267
268
269# Test the 9 Calltip methods.
270# open_calltip is about half the code; the others are fairly trivial.
271# The default mocks are what are needed for open_calltip.
272
273class mock_Shell:
274    "Return mock sufficient to pass to hyperparser."
275    def __init__(self, text):
276        text.tag_prevrange = Mock(return_value=None)
277        self.text = text
278        self.prompt_last_line = ">>> "
279        self.indentwidth = 4
280        self.tabwidth = 8
281
282
283class mock_TipWindow:
284    def __init__(self):
285        pass
286
287    def showtip(self, text, parenleft, parenright):
288        self.args = parenleft, parenright
289        self.parenline, self.parencol = map(int, parenleft.split('.'))
290
291
292class WrappedCalltip(calltip.Calltip):
293    def _make_tk_calltip_window(self):
294        return mock_TipWindow()
295
296    def remove_calltip_window(self, event=None):
297        if self.active_calltip:  # Setup to None.
298            self.active_calltip = None
299            self.tips_removed += 1  # Setup to 0.
300
301    def fetch_tip(self, expression):
302        return 'tip'
303
304
305class CalltipTest(unittest.TestCase):
306
307    @classmethod
308    def setUpClass(cls):
309        cls.text = Text()
310        cls.ct = WrappedCalltip(mock_Shell(cls.text))
311
312    def setUp(self):
313        self.text.delete('1.0', 'end')  # Insert and call
314        self.ct.active_calltip = None
315        # Test .active_calltip, +args
316        self.ct.tips_removed = 0
317
318    def open_close(self, testfunc):
319        # Open-close template with testfunc called in between.
320        opentip = self.ct.open_calltip
321        self.text.insert(1.0, 'f(')
322        opentip(False)
323        self.tip = self.ct.active_calltip
324        testfunc(self)  ###
325        self.text.insert('insert', ')')
326        opentip(False)
327        self.assertIsNone(self.ct.active_calltip, None)
328
329    def test_open_close(self):
330        def args(self):
331            self.assertEqual(self.tip.args, ('1.1', '1.end'))
332        self.open_close(args)
333
334    def test_repeated_force(self):
335        def force(self):
336            for char in 'abc':
337                self.text.insert('insert', 'a')
338                self.ct.open_calltip(True)
339                self.ct.open_calltip(True)
340            self.assertIs(self.ct.active_calltip, self.tip)
341        self.open_close(force)
342
343    def test_repeated_parens(self):
344        def parens(self):
345            for context in "a", "'":
346                with self.subTest(context=context):
347                    self.text.insert('insert', context)
348                    for char in '(()())':
349                        self.text.insert('insert', char)
350                    self.assertIs(self.ct.active_calltip, self.tip)
351            self.text.insert('insert', "'")
352        self.open_close(parens)
353
354    def test_comment_parens(self):
355        def comment(self):
356            self.text.insert('insert', "# ")
357            for char in '(()())':
358                self.text.insert('insert', char)
359            self.assertIs(self.ct.active_calltip, self.tip)
360            self.text.insert('insert', "\n")
361        self.open_close(comment)
362
363
364if __name__ == '__main__':
365    unittest.main(verbosity=2)
366