diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 4eef2b55880..440953d0690 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -898,7 +898,7 @@ def rectify_completions(text: str, completions: _IC, *, _debug: bool = False) -> new_text = text[new_start:c.start] + c.text + text[c.end:new_end] if c._origin == 'jedi': seen_jedi.add(new_text) - elif c._origin == 'IPCompleter.python_matches': + elif c._origin == "IPCompleter.python_matcher": seen_python_matches.add(new_text) yield Completion(new_start, new_end, new_text, type=c.type, _origin=c._origin, signature=c.signature) diff = seen_python_matches.difference(seen_jedi) @@ -1139,15 +1139,18 @@ def attr_matches(self, text): with a __getattr__ hook is evaluated. """ + return self._attr_matches(text)[0] + + def _attr_matches(self, text, include_prefix=True) -> Tuple[Sequence[str], str]: m2 = re.match(r"(.+)\.(\w*)$", self.line_buffer) if not m2: - return [] + return [], "" expr, attr = m2.group(1, 2) obj = self._evaluate_expr(expr) if obj is not_found: - return [] + return [], "" if self.limit_to__all__ and hasattr(obj, '__all__'): words = get__all__entries(obj) @@ -1170,28 +1173,36 @@ def attr_matches(self, text): # reconciliator would know that we intend to append to rather than # replace the input text; this requires refactoring to return range # which ought to be replaced (as does jedi). - tokens = _parse_tokens(expr) - rev_tokens = reversed(tokens) - skip_over = {tokenize.ENDMARKER, tokenize.NEWLINE} - name_turn = True - - parts = [] - for token in rev_tokens: - if token.type in skip_over: - continue - if token.type == tokenize.NAME and name_turn: - parts.append(token.string) - name_turn = False - elif token.type == tokenize.OP and token.string == "." and not name_turn: - parts.append(token.string) - name_turn = True - else: - # short-circuit if not empty nor name token - break + if include_prefix: + tokens = _parse_tokens(expr) + rev_tokens = reversed(tokens) + skip_over = {tokenize.ENDMARKER, tokenize.NEWLINE} + name_turn = True + + parts = [] + for token in rev_tokens: + if token.type in skip_over: + continue + if token.type == tokenize.NAME and name_turn: + parts.append(token.string) + name_turn = False + elif ( + token.type == tokenize.OP and token.string == "." and not name_turn + ): + parts.append(token.string) + name_turn = True + else: + # short-circuit if not empty nor name token + break - prefix_after_space = "".join(reversed(parts)) + prefix_after_space = "".join(reversed(parts)) + else: + prefix_after_space = "" - return ["%s.%s" % (prefix_after_space, w) for w in words if w[:n] == attr] + return ( + ["%s.%s" % (prefix_after_space, w) for w in words if w[:n] == attr], + "." + attr, + ) def _evaluate_expr(self, expr): obj = not_found @@ -1973,9 +1984,8 @@ def matchers(self) -> List[Matcher]: *self.magic_arg_matchers, self.custom_completer_matcher, self.dict_key_matcher, - # TODO: convert python_matches to v2 API self.magic_matcher, - self.python_matches, + self.python_matcher, self.file_matcher, self.python_func_kw_matcher, ] @@ -2316,9 +2326,42 @@ def _jedi_matches( else: return iter([]) + @context_matcher() + def python_matcher(self, context: CompletionContext) -> SimpleMatcherResult: + """Match attributes or global python names""" + text = context.line_with_cursor + if "." in text: + try: + matches, fragment = self._attr_matches(text, include_prefix=False) + if text.endswith(".") and self.omit__names: + if self.omit__names == 1: + # true if txt is _not_ a __ name, false otherwise: + no__name = lambda txt: re.match(r".*\.__.*?__", txt) is None + else: + # true if txt is _not_ a _ name, false otherwise: + no__name = ( + lambda txt: re.match(r"\._.*?", txt[txt.rindex(".") :]) + is None + ) + matches = filter(no__name, matches) + return _convert_matcher_v1_result_to_v2( + matches, type="attribute", fragment=fragment + ) + except NameError: + # catches . + matches = [] + return _convert_matcher_v1_result_to_v2(matches, type="attribute") + else: + matches = self.global_matches(context.token) + # TODO: maybe distinguish between functions, modules and just "variables" + return _convert_matcher_v1_result_to_v2(matches, type="variable") + @completion_matcher(api_version=1) def python_matches(self, text: str) -> Iterable[str]: - """Match attributes or global python names""" + """Match attributes or global python names. + + .. deprecated:: 8.27 + You can use :meth:`python_matcher` instead.""" if "." in text: try: matches = self.attr_matches(text) diff --git a/IPython/core/magics/pylab.py b/IPython/core/magics/pylab.py index 265f860063a..423498d404c 100644 --- a/IPython/core/magics/pylab.py +++ b/IPython/core/magics/pylab.py @@ -100,7 +100,7 @@ def matplotlib(self, line=''): % _list_matplotlib_backends_and_gui_loops() ) else: - gui, backend = self.shell.enable_matplotlib(args.gui.lower() if isinstance(args.gui, str) else args.gui) + gui, backend = self.shell.enable_matplotlib(args.gui) self._show_matplotlib_backend(args.gui, backend) @skip_doctest diff --git a/IPython/core/prefilter.py b/IPython/core/prefilter.py index e611b4b8340..a29df0c27ad 100644 --- a/IPython/core/prefilter.py +++ b/IPython/core/prefilter.py @@ -476,8 +476,8 @@ def check(self, line_info): any python operator, we should simply execute the line (regardless of whether or not there's a possible autocall expansion). This avoids spurious (and very confusing) geattr() accesses.""" - if line_info.the_rest and line_info.the_rest[0] in '!=()<>,+*/%^&|': - return self.prefilter_manager.get_handler_by_name('normal') + if line_info.the_rest and line_info.the_rest[0] in "!=()<>,+*/%^&|": + return self.prefilter_manager.get_handler_by_name("normal") else: return None @@ -512,6 +512,8 @@ def check(self, line_info): callable(oinfo.obj) and (not self.exclude_regexp.match(line_info.the_rest)) and self.function_name_regexp.match(line_info.ifun) + and line_info.raw_the_rest.startswith(" ") + or not line_info.raw_the_rest.strip() ): return self.prefilter_manager.get_handler_by_name("auto") else: diff --git a/IPython/core/profiledir.py b/IPython/core/profiledir.py index 1e33b552fb7..a1b94f2da33 100644 --- a/IPython/core/profiledir.py +++ b/IPython/core/profiledir.py @@ -14,6 +14,8 @@ from ..utils.path import expand_path, ensure_dir_exists from traitlets import Unicode, Bool, observe +from typing import Optional + #----------------------------------------------------------------------------- # Module errors #----------------------------------------------------------------------------- @@ -68,18 +70,31 @@ def _location_changed(self, change): self.pid_dir = os.path.join(new, self.pid_dir_name) self.static_dir = os.path.join(new, self.static_dir_name) self.check_dirs() - - def _mkdir(self, path, mode=None): + + def _mkdir(self, path: str, mode: Optional[int] = None) -> bool: """ensure a directory exists at a given path This is a version of os.mkdir, with the following differences: - - returns True if it created the directory, False otherwise + - returns whether the directory has been created or not. - ignores EEXIST, protecting against race conditions where the dir may have been created in between the check and the creation - sets permissions if requested and the dir already exists + + Parameters + ---------- + path: str + path of the dir to create + mode: int + see `mode` of `os.mkdir` + + Returns + ------- + bool: + returns True if it created the directory, False otherwise """ + if os.path.exists(path): if mode and os.stat(path).st_mode != mode: try: @@ -109,16 +124,20 @@ def check_log_dir(self, change=None): @observe('startup_dir') def check_startup_dir(self, change=None): - self._mkdir(self.startup_dir) - - readme = os.path.join(self.startup_dir, 'README') - src = os.path.join(get_ipython_package_dir(), u'core', u'profile', u'README_STARTUP') - - if not os.path.exists(src): - self.log.warning("Could not copy README_STARTUP to startup dir. Source file %s does not exist.", src) - - if os.path.exists(src) and not os.path.exists(readme): - shutil.copy(src, readme) + if self._mkdir(self.startup_dir): + readme = os.path.join(self.startup_dir, "README") + src = os.path.join( + get_ipython_package_dir(), "core", "profile", "README_STARTUP" + ) + + if os.path.exists(src): + if not os.path.exists(readme): + shutil.copy(src, readme) + else: + self.log.warning( + "Could not copy README_STARTUP to startup dir. Source file %s does not exist.", + src, + ) @observe('security_dir') def check_security_dir(self, change=None): diff --git a/IPython/core/release.py b/IPython/core/release.py index b72524d6ff8..c77f561096b 100644 --- a/IPython/core/release.py +++ b/IPython/core/release.py @@ -16,7 +16,7 @@ # release. 'dev' as a _version_extra string means this is a development # version _version_major = 8 -_version_minor = 26 +_version_minor = 27 _version_patch = 0 _version_extra = ".dev" # _version_extra = "rc1" diff --git a/IPython/core/splitinput.py b/IPython/core/splitinput.py index 0cd70ec9100..33e462b3b80 100644 --- a/IPython/core/splitinput.py +++ b/IPython/core/splitinput.py @@ -76,7 +76,7 @@ def split_user_input(line, pattern=None): # print('line:<%s>' % line) # dbg # print('pre <%s> ifun <%s> rest <%s>' % (pre,ifun.strip(),the_rest)) # dbg - return pre, esc or '', ifun.strip(), the_rest.lstrip() + return pre, esc or "", ifun.strip(), the_rest class LineInfo(object): @@ -107,11 +107,15 @@ class LineInfo(object): the_rest Everything else on the line. + + raw_the_rest + the_rest without whitespace stripped. """ def __init__(self, line, continue_prompt=False): self.line = line self.continue_prompt = continue_prompt - self.pre, self.esc, self.ifun, self.the_rest = split_user_input(line) + self.pre, self.esc, self.ifun, self.raw_the_rest = split_user_input(line) + self.the_rest = self.raw_the_rest.lstrip() self.pre_char = self.pre.strip() if self.pre_char: @@ -136,3 +140,6 @@ def ofind(self, ip) -> OInfo: def __str__(self): return "LineInfo [%s|%s|%s|%s]" %(self.pre, self.esc, self.ifun, self.the_rest) + + def __repr__(self): + return "<" + str(self) + ">" diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 87c561dc06b..e20d9b88a89 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -462,7 +462,10 @@ def test_all_completions_dups(self): matches = c.all_completions("TestClass.") assert len(matches) > 2, (jedi_status, matches) matches = c.all_completions("TestClass.a") - assert matches == ['TestClass.a', 'TestClass.a1'], jedi_status + if jedi_status: + assert matches == ["TestClass.a", "TestClass.a1"], jedi_status + else: + assert matches == [".a", ".a1"], jedi_status @pytest.mark.xfail( sys.version_info.releaselevel in ("alpha",), @@ -594,7 +597,7 @@ def _(line, cursor_pos, expect, message, completion): ip.Completer.use_jedi = True with provisionalcompleter(): completions = ip.Completer.completions(line, cursor_pos) - self.assertIn(completion, completions) + self.assertIn(completion, list(completions)) with provisionalcompleter(): _( @@ -622,7 +625,7 @@ def _(line, cursor_pos, expect, message, completion): _( "assert str.star", 14, - "str.startswith", + ".startswith", "Should have completed on `assert str.star`: %s", Completion(11, 14, "startswith"), ) @@ -633,6 +636,13 @@ def _(line, cursor_pos, expect, message, completion): "Should have completed on `d['a b'].str`: %s", Completion(9, 12, "strip"), ) + _( + "a.app", + 4, + ".append", + "Should have completed on `a.app`: %s", + Completion(2, 4, "append"), + ) def test_omit__names(self): # also happens to test IPCompleter as a configurable @@ -647,8 +657,8 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip.") - self.assertIn("ip.__str__", matches) - self.assertIn("ip._hidden_attr", matches) + self.assertIn(".__str__", matches) + self.assertIn("._hidden_attr", matches) # c.use_jedi = True # completions = set(c.completions('ip.', 3)) @@ -661,7 +671,7 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip.") - self.assertNotIn("ip.__str__", matches) + self.assertNotIn(".__str__", matches) # self.assertIn('ip._hidden_attr', matches) # c.use_jedi = True @@ -675,8 +685,8 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip.") - self.assertNotIn("ip.__str__", matches) - self.assertNotIn("ip._hidden_attr", matches) + self.assertNotIn(".__str__", matches) + self.assertNotIn("._hidden_attr", matches) # c.use_jedi = True # completions = set(c.completions('ip.', 3)) @@ -686,7 +696,7 @@ def test_omit__names(self): with provisionalcompleter(): c.use_jedi = False s, matches = c.complete("ip._x.") - self.assertIn("ip._x.keys", matches) + self.assertIn(".keys", matches) # c.use_jedi = True # completions = set(c.completions('ip._x.', 6)) @@ -697,7 +707,7 @@ def test_omit__names(self): def test_limit_to__all__False_ok(self): """ - Limit to all is deprecated, once we remove it this test can go away. + Limit to all is deprecated, once we remove it this test can go away. """ ip = get_ipython() c = ip.Completer @@ -708,7 +718,7 @@ def test_limit_to__all__False_ok(self): cfg.IPCompleter.limit_to__all__ = False c.update_config(cfg) s, matches = c.complete("d.") - self.assertIn("d.x", matches) + self.assertIn(".x", matches) def test_get__all__entries_ok(self): class A: diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index b22be9231dc..6113ff920c4 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -59,40 +59,43 @@ def test_ipdb_magics(): First, set up some test functions and classes which we can inspect. - >>> class ExampleClass(object): - ... """Docstring for ExampleClass.""" - ... def __init__(self): - ... """Docstring for ExampleClass.__init__""" - ... pass - ... def __str__(self): - ... return "ExampleClass()" - - >>> def example_function(x, y, z="hello"): - ... """Docstring for example_function.""" - ... pass + In [1]: class ExampleClass(object): + ...: """Docstring for ExampleClass.""" + ...: def __init__(self): + ...: """Docstring for ExampleClass.__init__""" + ...: pass + ...: def __str__(self): + ...: return "ExampleClass()" - >>> old_trace = sys.gettrace() + In [2]: def example_function(x, y, z="hello"): + ...: """Docstring for example_function.""" + ...: pass - Create a function which triggers ipdb. + In [3]: old_trace = sys.gettrace() - >>> def trigger_ipdb(): - ... a = ExampleClass() - ... debugger.Pdb().set_trace() + Create a function which triggers ipdb. - >>> with PdbTestInput([ - ... 'pdef example_function', - ... 'pdoc ExampleClass', - ... 'up', - ... 'down', - ... 'list', - ... 'pinfo a', - ... 'll', - ... 'continue', - ... ]): - ... trigger_ipdb() - --Return-- - None - > (3)trigger_ipdb() + In [4]: def trigger_ipdb(): + ...: a = ExampleClass() + ...: debugger.Pdb().set_trace() + + Run ipdb with faked input & check output. Because of a difference between + Python 3.13 & older versions, the first bit of the output is inconsistent. + We need to use ... to accommodate that, so the examples have to use IPython + prompts so that ... is distinct from the Python PS2 prompt. + + In [5]: with PdbTestInput([ + ...: 'pdef example_function', + ...: 'pdoc ExampleClass', + ...: 'up', + ...: 'down', + ...: 'list', + ...: 'pinfo a', + ...: 'll', + ...: 'continue', + ...: ]): + ...: trigger_ipdb() + ...> (3)trigger_ipdb() 1 def trigger_ipdb(): 2 a = ExampleClass() ----> 3 debugger.Pdb().set_trace() @@ -112,8 +115,7 @@ def test_ipdb_magics(): 10 ]): ---> 11 trigger_ipdb() - ipdb> down - None + ipdb> down... > (3)trigger_ipdb() 1 def trigger_ipdb(): 2 a = ExampleClass() @@ -136,10 +138,10 @@ def test_ipdb_magics(): ----> 3 debugger.Pdb().set_trace() ipdb> continue - - Restore previous trace function, e.g. for coverage.py - - >>> sys.settrace(old_trace) + + Restore previous trace function, e.g. for coverage.py + + In [6]: sys.settrace(old_trace) ''' def test_ipdb_magics2(): @@ -495,15 +497,26 @@ def test_decorator_skip_with_breakpoint(): child.expect_exact(line) child.sendline("") - # as the filename does not exists, we'll rely on the filename prompt - child.expect_exact("47 bar(3, 4)") - - for input_, expected in [ - (f"b {name}.py:3", ""), - ("step", "1---> 3 pass # should not stop here except"), - ("step", "---> 38 @pdb_skipped_decorator"), - ("continue", ""), - ]: + # From 3.13, set_trace()/breakpoint() stop on the line where they're + # called, instead of the next line. + if sys.version_info >= (3, 13): + child.expect_exact("--> 46 ipdb.set_trace()") + extra_step = [("step", "--> 47 bar(3, 4)")] + else: + child.expect_exact("--> 47 bar(3, 4)") + extra_step = [] + + for input_, expected in ( + [ + (f"b {name}.py:3", ""), + ] + + extra_step + + [ + ("step", "1---> 3 pass # should not stop here except"), + ("step", "---> 38 @pdb_skipped_decorator"), + ("continue", ""), + ] + ): child.expect("ipdb>") child.sendline(input_) child.expect_exact(input_) diff --git a/IPython/core/tests/test_handlers.py b/IPython/core/tests/test_handlers.py index 604dadee1ab..905d9abe07b 100644 --- a/IPython/core/tests/test_handlers.py +++ b/IPython/core/tests/test_handlers.py @@ -7,17 +7,13 @@ # our own packages from IPython.core import autocall from IPython.testing import tools as tt +import pytest +from collections.abc import Callable #----------------------------------------------------------------------------- # Globals #----------------------------------------------------------------------------- -# Get the public instance of IPython - -failures = [] -num_tests = 0 - -#----------------------------------------------------------------------------- # Test functions #----------------------------------------------------------------------------- @@ -31,67 +27,49 @@ def __call__(self): return "called" -def run(tests): - """Loop through a list of (pre, post) inputs, where pre is the string - handed to ipython, and post is how that string looks after it's been - transformed (i.e. ipython's notion of _i)""" - tt.check_pairs(ip.prefilter_manager.prefilter_lines, tests) - +@pytest.mark.parametrize( + "autocall, input, output", + [ + # For many of the below, we're also checking that leading whitespace + # turns off the esc char, which it should unless there is a continuation + # line. + ("1", '"no change"', '"no change"'), # normal + ("1", "lsmagic", "get_ipython().run_line_magic('lsmagic', '')"), # magic + # Only explicit escapes or instances of IPyAutocallable should get + # expanded + ("0", 'len "abc"', 'len "abc"'), + ("0", "autocallable", "autocallable()"), + # Don't add extra brackets (gh-1117) + ("0", "autocallable()", "autocallable()"), + ("1", 'len "abc"', 'len("abc")'), + ("1", 'len "abc";', 'len("abc");'), # ; is special -- moves out of parens + # Autocall is turned off if first arg is [] and the object + # is both callable and indexable. Like so: + ("1", "len [1,2]", "len([1,2])"), # len doesn't support __getitem__... + ("1", "call_idx [1]", "call_idx [1]"), # call_idx *does*.. + ("1", "call_idx 1", "call_idx(1)"), + ("1", "len", "len"), # only at 2 does it auto-call on single args + ("2", 'len "abc"', 'len("abc")'), + ("2", 'len "abc";', 'len("abc");'), + ("2", "len [1,2]", "len([1,2])"), + ("2", "call_idx [1]", "call_idx [1]"), + ("2", "call_idx 1", "call_idx(1)"), + # T his is what's different: + ("2", "len", "len()"), # only at 2 does it auto-call on single args + ("0", "Callable[[int], None]", "Callable[[int], None]"), + ("1", "Callable[[int], None]", "Callable[[int], None]"), + ("1", "Callable[[int], None]", "Callable[[int], None]"), + ], +) +def test_handlers_I(autocall, input, output): + autocallable = Autocallable() + ip.user_ns["autocallable"] = autocallable -def test_handlers(): call_idx = CallableIndexable() - ip.user_ns['call_idx'] = call_idx + ip.user_ns["call_idx"] = call_idx - # For many of the below, we're also checking that leading whitespace - # turns off the esc char, which it should unless there is a continuation - # line. - run( - [('"no change"', '"no change"'), # normal - (u"lsmagic", "get_ipython().run_line_magic('lsmagic', '')"), # magic - #("a = b # PYTHON-MODE", '_i'), # emacs -- avoids _in cache - ]) + ip.user_ns["Callable"] = Callable - # Objects which are instances of IPyAutocall are *always* autocalled - autocallable = Autocallable() - ip.user_ns['autocallable'] = autocallable - - # auto - ip.run_line_magic("autocall", "0") - # Only explicit escapes or instances of IPyAutocallable should get - # expanded - run( - [ - ('len "abc"', 'len "abc"'), - ("autocallable", "autocallable()"), - # Don't add extra brackets (gh-1117) - ("autocallable()", "autocallable()"), - ] - ) + ip.run_line_magic("autocall", autocall) + assert ip.prefilter_manager.prefilter_lines(input) == output ip.run_line_magic("autocall", "1") - run( - [ - ('len "abc"', 'len("abc")'), - ('len "abc";', 'len("abc");'), # ; is special -- moves out of parens - # Autocall is turned off if first arg is [] and the object - # is both callable and indexable. Like so: - ("len [1,2]", "len([1,2])"), # len doesn't support __getitem__... - ("call_idx [1]", "call_idx [1]"), # call_idx *does*.. - ("call_idx 1", "call_idx(1)"), - ("len", "len"), # only at 2 does it auto-call on single args - ] - ) - ip.run_line_magic("autocall", "2") - run( - [ - ('len "abc"', 'len("abc")'), - ('len "abc";', 'len("abc");'), - ("len [1,2]", "len([1,2])"), - ("call_idx [1]", "call_idx [1]"), - ("call_idx 1", "call_idx(1)"), - # This is what's different: - ("len", "len()"), # only at 2 does it auto-call on single args - ] - ) - ip.run_line_magic("autocall", "1") - - assert failures == [] diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index 3decb8be04b..ac7c3656099 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -516,23 +516,23 @@ def prop(self, v): ip.run_line_magic("pinfo", "b.prop") captured = capsys.readouterr() - assert "Docstring: cdoc for prop" in captured.out + assert re.search(r"Docstring:\s+cdoc for prop", captured.out) ip.run_line_magic("pinfo", "b.non_exist") captured = capsys.readouterr() - assert "Docstring: cdoc for non_exist" in captured.out + assert re.search(r"Docstring:\s+cdoc for non_exist", captured.out) ip.run_cell("b.prop?") captured = capsys.readouterr() - assert "Docstring: cdoc for prop" in captured.out + assert re.search(r"Docstring:\s+cdoc for prop", captured.out) ip.run_cell("b.non_exist?") captured = capsys.readouterr() - assert "Docstring: cdoc for non_exist" in captured.out + assert re.search(r"Docstring:\s+cdoc for non_exist", captured.out) ip.run_cell("b.undefined?") captured = capsys.readouterr() - assert "Docstring: " in captured.out + assert re.search(r"Type:\s+NoneType", captured.out) def test_pinfo_magic(): diff --git a/IPython/core/tests/test_prefilter.py b/IPython/core/tests/test_prefilter.py index 91c3c868821..999cd43e6e8 100644 --- a/IPython/core/tests/test_prefilter.py +++ b/IPython/core/tests/test_prefilter.py @@ -48,7 +48,7 @@ def dummy_magic(line): pass def test_autocall_binops(): """See https://github.com/ipython/ipython/issues/81""" - ip.magic('autocall 2') + ip.run_line_magic("autocall", "2") f = lambda x: x ip.user_ns['f'] = f try: @@ -71,8 +71,8 @@ def test_autocall_binops(): finally: pm.unregister_checker(ac) finally: - ip.magic('autocall 0') - del ip.user_ns['f'] + ip.run_line_magic("autocall", "0") + del ip.user_ns["f"] def test_issue_114(): @@ -105,23 +105,35 @@ def __call__(self, x): return x # Create a callable broken object - ip.user_ns['x'] = X() - ip.magic('autocall 2') + ip.user_ns["x"] = X() + ip.run_line_magic("autocall", "2") try: # Even if x throws an attribute error when looking at its rewrite # attribute, we should not crash. So the test here is simply making # the prefilter call and not having an exception. ip.prefilter('x 1') finally: - del ip.user_ns['x'] - ip.magic('autocall 0') + del ip.user_ns["x"] + ip.run_line_magic("autocall", "0") + + +def test_autocall_type_ann(): + ip.run_cell("import collections.abc") + ip.run_line_magic("autocall", "1") + try: + assert ( + ip.prefilter("collections.abc.Callable[[int], None]") + == "collections.abc.Callable[[int], None]" + ) + finally: + ip.run_line_magic("autocall", "0") def test_autocall_should_support_unicode(): - ip.magic('autocall 2') - ip.user_ns['π'] = lambda x: x + ip.run_line_magic("autocall", "2") + ip.user_ns["π"] = lambda x: x try: assert ip.prefilter("π 3") == "π(3)" finally: - ip.magic('autocall 0') - del ip.user_ns['π'] + ip.run_line_magic("autocall", "0") + del ip.user_ns["π"] diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py index 6bddb348077..31d3dbe21f2 100644 --- a/IPython/core/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -258,6 +258,29 @@ def test_qt_gtk(self): assert gui == "qt" assert s.pylab_gui_select == "qt" + @dec.skipif(not pt._matplotlib_manages_backends()) + def test_backend_module_name_case_sensitive(self): + # Matplotlib backend names are case insensitive unless explicitly specified using + # "module://some_module.some_name" syntax which are case sensitive for mpl >= 3.9.1 + all_lowercase = "module://matplotlib_inline.backend_inline" + some_uppercase = "module://matplotlib_inline.Backend_inline" + mpl3_9_1 = matplotlib.__version_info__ >= (3, 9, 1) + + s = self.Shell() + s.enable_matplotlib(all_lowercase) + if mpl3_9_1: + with pytest.raises(RuntimeError): + s.enable_matplotlib(some_uppercase) + else: + s.enable_matplotlib(some_uppercase) + + s.run_line_magic("matplotlib", all_lowercase) + if mpl3_9_1: + with pytest.raises(RuntimeError): + s.run_line_magic("matplotlib", some_uppercase) + else: + s.run_line_magic("matplotlib", some_uppercase) + def test_no_gui_backends(): for k in ['agg', 'svg', 'pdf', 'ps']: diff --git a/IPython/core/tests/test_splitinput.py b/IPython/core/tests/test_splitinput.py index 1462e7fa033..f5fc53fafe3 100644 --- a/IPython/core/tests/test_splitinput.py +++ b/IPython/core/tests/test_splitinput.py @@ -1,7 +1,8 @@ # coding: utf-8 from IPython.core.splitinput import split_user_input, LineInfo -from IPython.testing import tools as tt + +import pytest tests = [ ("x=1", ("", "", "x", "=1")), @@ -19,18 +20,19 @@ (";ls", ("", ";", "ls", "")), (" ;ls", (" ", ";", "ls", "")), ("f.g(x)", ("", "", "f.g", "(x)")), - ("f.g (x)", ("", "", "f.g", "(x)")), + ("f.g (x)", ("", "", "f.g", " (x)")), ("?%hist1", ("", "?", "%hist1", "")), ("?%%hist2", ("", "?", "%%hist2", "")), ("??%hist3", ("", "??", "%hist3", "")), ("??%%hist4", ("", "??", "%%hist4", "")), ("?x*", ("", "?", "x*", "")), + ("Pérez Fernando", ("", "", "Pérez", " Fernando")), ] -tests.append(("Pérez Fernando", ("", "", "Pérez", "Fernando"))) -def test_split_user_input(): - return tt.check_pairs(split_user_input, tests) +@pytest.mark.parametrize("input, output", tests) +def test_split_user_input(input, output): + assert split_user_input(input) == output def test_LineInfo(): diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index e167d99506a..8ed73873aa1 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -298,6 +298,13 @@ class Python3ChainedExceptionsTest(unittest.TestCase): raise ValueError("Yikes") from None """ + SYS_EXIT_WITH_CONTEXT_CODE = """ +try: + 1/0 +except Exception as e: + raise SystemExit(1) + """ + def test_direct_cause_error(self): with tt.AssertPrints(["KeyError", "NameError", "direct cause"]): ip.run_cell(self.DIRECT_CAUSE_ERROR_CODE) @@ -306,6 +313,11 @@ def test_exception_during_handling_error(self): with tt.AssertPrints(["KeyError", "NameError", "During handling"]): ip.run_cell(self.EXCEPTION_DURING_HANDLING_CODE) + def test_sysexit_while_handling_error(self): + with tt.AssertPrints(["SystemExit", "to see the full traceback"]): + with tt.AssertNotPrints(["another exception"], suppress=False): + ip.run_cell(self.SYS_EXIT_WITH_CONTEXT_CODE) + def test_suppress_exception_chaining(self): with tt.AssertNotPrints("ZeroDivisionError"), \ tt.AssertPrints("ValueError", suppress=False): diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index cc139b1e257..66c9ce9108b 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -552,28 +552,31 @@ def structured_traceback( lines = ''.join(self._format_exception_only(etype, evalue)) out_list.append(lines) - exception = self.get_parts_of_chained_exception(evalue) + # Find chained exceptions if we have a traceback (not for exception-only mode) + if etb is not None: + exception = self.get_parts_of_chained_exception(evalue) - if exception and (id(exception[1]) not in chained_exc_ids): - chained_exception_message = ( - self.prepare_chained_exception_message(evalue.__cause__)[0] - if evalue is not None - else "" - ) - etype, evalue, etb = exception - # Trace exception to avoid infinite 'cause' loop - chained_exc_ids.add(id(exception[1])) - chained_exceptions_tb_offset = 0 - out_list = ( - self.structured_traceback( - etype, - evalue, - (etb, chained_exc_ids), # type: ignore - chained_exceptions_tb_offset, - context, + if exception and (id(exception[1]) not in chained_exc_ids): + chained_exception_message = ( + self.prepare_chained_exception_message(evalue.__cause__)[0] + if evalue is not None + else "" + ) + etype, evalue, etb = exception + # Trace exception to avoid infinite 'cause' loop + chained_exc_ids.add(id(exception[1])) + chained_exceptions_tb_offset = 0 + out_list = ( + self.structured_traceback( + etype, + evalue, + (etb, chained_exc_ids), # type: ignore + chained_exceptions_tb_offset, + context, + ) + + chained_exception_message + + out_list ) - + chained_exception_message - + out_list) return out_list diff --git a/docs/source/config/custommagics.rst b/docs/source/config/custommagics.rst index 0a37b858a4c..4854970ef31 100644 --- a/docs/source/config/custommagics.rst +++ b/docs/source/config/custommagics.rst @@ -141,7 +141,7 @@ Accessing user namespace and local scope When creating line magics, you may need to access surrounding scope to get user variables (e.g when called inside functions). IPython provides the ``@needs_local_scope`` decorator that can be imported from -``IPython.core.magics``. When decorated with ``@needs_local_scope`` a magic will +``IPython.core.magic``. When decorated with ``@needs_local_scope`` a magic will be passed ``local_ns`` as an argument. As a convenience ``@needs_local_scope`` can also be applied to cell magics even if cell magics cannot appear at local scope context. @@ -153,7 +153,7 @@ Sometimes it may be useful to define a magic that can be silenced the same way that non-magic expressions can, i.e., by appending a semicolon at the end of the Python code to be executed. That can be achieved by decorating the magic function with the decorator ``@output_can_be_silenced`` that can be imported from -``IPython.core.magics``. When this decorator is used, IPython will parse the Python +``IPython.core.magic``. When this decorator is used, IPython will parse the Python code used by the magic and, if the last token is a ``;``, the output created by the magic will not show up on the screen. If you want to see an example of this decorator in action, take a look on the ``time`` magic defined in diff --git a/docs/source/whatsnew/version8.rst b/docs/source/whatsnew/version8.rst index 490bec850ea..ba09695c266 100644 --- a/docs/source/whatsnew/version8.rst +++ b/docs/source/whatsnew/version8.rst @@ -1,6 +1,37 @@ ============ 8.x Series ============ +.. _version 8.27: + +IPython 8.27 +============ + +New release of IPython after a month off (not enough changes). We can see a few +important changes for this release. + + - autocall was beeing call getitem, :ghpull:`14486` + - Only copy files in startup dir if we just created it. :ghpull:`14497` + - Fix some tests on Python 3.13 RC1 :ghpull:`14504`; this one I guess make this + the first IPython release officially compatible with Python 3.13; you will + need the most recent ``executing`` and ``stack_data``, we won't pin to avoid + forcing user of older Python version to upgrade. + + +As usual you can find the full list of PRs on GitHub under `the 8.27 +`__ milestone. + +Thanks +------ + +Many thanks to `@Kleirre `__ our June intern for +doing her first contribution to open source, doing the releases notes and +release. I guess you didn't even notice it was not me who released :-). I wish +her all the best in her future endeavor and look forward for her work in +astrophysics. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + .. _version 8.26: IPython 8.26 diff --git a/tools/release b/tools/release index ffc7e63cb65..5164a2bb699 100755 --- a/tools/release +++ b/tools/release @@ -1,86 +1,10 @@ #!/usr/bin/env python3 """IPython release script. -This should ONLY be run at real release time. -""" -from __future__ import print_function - -import os -from glob import glob -from pathlib import Path -from subprocess import call -import sys - -from toollib import (get_ipdir, cd, execfile, sh, archive, - archive_user, archive_dir) - -# Get main ipython dir, this will raise if it doesn't pass some checks -ipdir = get_ipdir() -tooldir = ipdir / 'tools' -distdir = ipdir / 'dist' - -# Where I keep static backups of each release -ipbackupdir = Path('~/ipython/backup').expanduser() -if not ipbackupdir.exists(): - ipbackupdir.mkdir(parents=True, exist_ok=True) - -# Start in main IPython dir -cd(ipdir) - -# Load release info -version = None -execfile(Path('IPython','core','release.py'), globals()) - -# Build site addresses for file uploads -release_site = '%s/release/%s' % (archive, version) -backup_site = '%s/backup/' % archive - -# Start actual release process -print() -print('Releasing IPython') -print('=================') -print() -print('Version:', version) -print() -print('Source IPython directory:', ipdir) -print() - -# Perform local backup, go to tools dir to run it. -cd(tooldir) +Deprecated -if 'upload' in sys.argv: - cd(distdir) - - # do not upload OS specific files like .DS_Store - to_upload = glob('*.whl')+glob('*.tar.gz') - - # Make target dir if it doesn't exist - print('1. Uploading IPython to archive.ipython.org') - sh('ssh %s "mkdir -p %s/release/%s" ' % (archive_user, archive_dir, version)) - sh('scp *.tar.gz *.whl %s' % release_site) - - print('2. Uploading backup files...') - cd(ipbackupdir) - sh('scp `ls -1tr *tgz | tail -1` %s' % backup_site) - - print('3. Uploading to PyPI using twine') - cd(distdir) - call(['twine', 'upload', '--verbose'] + to_upload) - -else: - # Build, but don't upload - - # Make backup tarball - sh('python make_tarball.py') - sh('mv ipython-*.tgz %s' % ipbackupdir) - - # Build release files - sh('./build_release') - - cd(ipdir) - - print("`./release upload` to upload source distribution on PyPI and ipython archive") - sys.exit(0) +""" +sys.exit("deprecated") diff --git a/tools/release_helper.sh b/tools/release_helper.sh index df1ca0d48d2..08c8f6b8571 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -231,7 +231,7 @@ then echo echo $BLUE"Attempting to build package..."$NOR - tools/release + tools/build_release echo $RED'$ shasum -a 256 dist/*' @@ -245,7 +245,7 @@ then echo echo $BLUE"Attempting to build package..."$NOR - tools/release + tools/build_release echo $RED"Check the shasum for SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH" echo $RED'$ shasum -a 256 dist/*' @@ -254,6 +254,6 @@ then if ask_section "upload packages ?" then - tools/release upload + twine upload --verbose dist/*.tar.gz dist/*.whl fi fi