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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy