diff --git a/IPython/core/magic.py b/IPython/core/magic.py index 5204eb592b..9fc361b532 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -22,7 +22,7 @@ from .inputtransformer2 import ESC_MAGIC, ESC_MAGIC2 from ..utils.ipstruct import Struct from ..utils.process import arg_split -from ..utils.text import dedent +import inspect from traitlets import Bool, Dict, Instance, observe from logging import error @@ -272,7 +272,7 @@ def mark(func: _F, *a: Any, **kw: Any) -> _F: # Ensure the resulting decorator has a usable docstring ds = _docstring_template.format("function", magic_kind) - ds += dedent( + ds += inspect.cleandoc( """ Note: this decorator can only be used in a context where IPython is already active, so that the `get_ipython()` call succeeds. You can therefore use diff --git a/IPython/core/magic_arguments.py b/IPython/core/magic_arguments.py index 4871f56d3d..bf2a384436 100644 --- a/IPython/core/magic_arguments.py +++ b/IPython/core/magic_arguments.py @@ -75,13 +75,14 @@ def my_cell_magic(line, cell): :parts: 3 ''' -#----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- # Copyright (C) 2010-2011, IPython Development Team. # # Distributed under the terms of the Modified BSD License. # # The full license is in the file COPYING.txt, distributed with this software. -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- import argparse import re @@ -89,22 +90,36 @@ def my_cell_magic(line, cell): from IPython.core.error import UsageError from IPython.utils.decorators import undoc from IPython.utils.process import arg_split -from IPython.utils.text import dedent +import textwrap + NAME_RE = re.compile(r"[a-zA-Z][a-zA-Z0-9_-]*$") + @undoc class MagicHelpFormatter(argparse.RawDescriptionHelpFormatter): - """A HelpFormatter with a couple of changes to meet our needs. - """ + """A HelpFormatter with a couple of changes to meet our needs.""" + # Modified to dedent text. def _fill_text(self, text, width, indent): - return argparse.RawDescriptionHelpFormatter._fill_text(self, dedent(text), width, indent) + # Dedent ignoring unindented first line (preserves original IPython behavior) + if not text.startswith("\n"): + splits = text.split("\n", 1) + if len(splits) == 2: + first, rest = splits + text = "\n".join([first, textwrap.dedent(rest)]) + else: + text = textwrap.dedent(text) + else: + text = textwrap.dedent(text) + return argparse.RawDescriptionHelpFormatter._fill_text( + self, text, width, indent + ) # Modified to wrap argument placeholders in <> where necessary. def _format_action_invocation(self, action): if not action.option_strings: - metavar, = self._metavar_formatter(action, action.dest)(1) + (metavar,) = self._metavar_formatter(action, action.dest)(1) return metavar else: @@ -125,45 +140,53 @@ def _format_action_invocation(self, action): if not NAME_RE.match(args_string): args_string = "<%s>" % args_string for option_string in action.option_strings: - parts.append('%s %s' % (option_string, args_string)) + parts.append("%s %s" % (option_string, args_string)) - return ', '.join(parts) + return ", ".join(parts) # Override the default prefix ('usage') to our % magic escape, # in a code block. def add_usage(self, usage, actions, groups, prefix="::\n\n %"): super(MagicHelpFormatter, self).add_usage(usage, actions, groups, prefix) + class MagicArgumentParser(argparse.ArgumentParser): - """ An ArgumentParser tweaked for use by IPython magics. - """ - def __init__(self, - prog=None, - usage=None, - description=None, - epilog=None, - parents=None, - formatter_class=MagicHelpFormatter, - prefix_chars='-', - argument_default=None, - conflict_handler='error', - add_help=False): + """An ArgumentParser tweaked for use by IPython magics.""" + + def __init__( + self, + prog=None, + usage=None, + description=None, + epilog=None, + parents=None, + formatter_class=MagicHelpFormatter, + prefix_chars="-", + argument_default=None, + conflict_handler="error", + add_help=False, + ): if parents is None: parents = [] - super(MagicArgumentParser, self).__init__(prog=prog, usage=usage, - description=description, epilog=epilog, - parents=parents, formatter_class=formatter_class, - prefix_chars=prefix_chars, argument_default=argument_default, - conflict_handler=conflict_handler, add_help=add_help) + super(MagicArgumentParser, self).__init__( + prog=prog, + usage=usage, + description=description, + epilog=epilog, + parents=parents, + formatter_class=formatter_class, + prefix_chars=prefix_chars, + argument_default=argument_default, + conflict_handler=conflict_handler, + add_help=add_help, + ) def error(self, message): - """ Raise a catchable error instead of exiting. - """ + """Raise a catchable error instead of exiting.""" raise UsageError(message) def parse_argstring(self, argstring, *, partial=False): - """ Split a string into an argument list and parse that argument list. - """ + """Split a string into an argument list and parse that argument list.""" argv = arg_split(argstring, strict=not partial) if partial: return self.parse_known_args(argv) @@ -171,61 +194,52 @@ def parse_argstring(self, argstring, *, partial=False): def construct_parser(magic_func): - """ Construct an argument parser using the function decorations. - """ - kwds = getattr(magic_func, 'argcmd_kwds', {}) - if 'description' not in kwds: - kwds['description'] = getattr(magic_func, '__doc__', None) + """Construct an argument parser using the function decorations.""" + kwds = getattr(magic_func, "argcmd_kwds", {}) + if "description" not in kwds: + kwds["description"] = getattr(magic_func, "__doc__", None) arg_name = real_name(magic_func) parser = MagicArgumentParser(arg_name, **kwds) - # Reverse the list of decorators in order to apply them in the - # order in which they appear in the source. group = None for deco in magic_func.decorators[::-1]: result = deco.add_to_parser(parser, group) if result is not None: group = result - # Replace the magic function's docstring with the full help text. magic_func.__doc__ = parser.format_help() return parser def parse_argstring(magic_func, argstring, *, partial=False): - """ Parse the string of arguments for the given magic function. - """ + """Parse the string of arguments for the given magic function.""" return magic_func.parser.parse_argstring(argstring, partial=partial) def real_name(magic_func): - """ Find the real name of the magic. - """ + """Find the real name of the magic.""" magic_name = magic_func.__name__ - if magic_name.startswith('magic_'): - magic_name = magic_name[len('magic_'):] - return getattr(magic_func, 'argcmd_name', magic_name) + if magic_name.startswith("magic_"): + magic_name = magic_name[len("magic_") :] + return getattr(magic_func, "argcmd_name", magic_name) class ArgDecorator: - """ Base class for decorators to add ArgumentParser information to a method. - """ + """Base class for decorators to add ArgumentParser information to a method.""" def __call__(self, func): - if not getattr(func, 'has_arguments', False): + if not getattr(func, "has_arguments", False): func.has_arguments = True func.decorators = [] func.decorators.append(self) return func def add_to_parser(self, parser, group): - """ Add this object's information to the parser, if necessary. - """ pass class magic_arguments(ArgDecorator): - """ Mark the magic as having argparse arguments and possibly adjust the + """Mark the magic as having argparse arguments and possibly adjust the name. """ @@ -233,19 +247,16 @@ def __init__(self, name=None): self.name = name def __call__(self, func): - if not getattr(func, 'has_arguments', False): + if not getattr(func, "has_arguments", False): func.has_arguments = True func.decorators = [] if self.name is not None: func.argcmd_name = self.name - # This should be the first decorator in the list of decorators, thus the - # last to execute. Build the parser. func.parser = construct_parser(func) return func class ArgMethodWrapper(ArgDecorator): - """ Base class to define a wrapper for ArgumentParser method. @@ -260,8 +271,6 @@ def __init__(self, *args, **kwds): self.kwds = kwds def add_to_parser(self, parser, group): - """ Add this object's information to the parser. - """ if group is not None: parser = group getattr(parser, self._method_name)(*self.args, **self.kwds) @@ -269,36 +278,36 @@ def add_to_parser(self, parser, group): class argument(ArgMethodWrapper): - """ Store arguments and keywords to pass to add_argument(). + """Store arguments and keywords to pass to add_argument(). Instances also serve to decorate command methods. """ - _method_name = 'add_argument' + + _method_name = "add_argument" class defaults(ArgMethodWrapper): - """ Store arguments and keywords to pass to set_defaults(). + """Store arguments and keywords to pass to set_defaults(). Instances also serve to decorate command methods. """ - _method_name = 'set_defaults' + + _method_name = "set_defaults" class argument_group(ArgMethodWrapper): - """ Store arguments and keywords to pass to add_argument_group(). + """Store arguments and keywords to pass to add_argument_group(). Instances also serve to decorate command methods. """ def add_to_parser(self, parser, group): - """ Add this object's information to the parser. - """ return parser.add_argument_group(*self.args, **self.kwds) class kwds(ArgDecorator): - """ Provide other keywords to the sub-parser constructor. - """ + """Provide other keywords to the sub-parser constructor.""" + def __init__(self, **kwds): self.kwds = kwds @@ -308,5 +317,4 @@ def __call__(self, func): return func -__all__ = ['magic_arguments', 'argument', 'argument_group', 'kwds', - 'parse_argstring'] +__all__ = ["magic_arguments", "argument", "argument_group", "kwds", "parse_argstring"] diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index 4ed8ae03cd..262052c0fa 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -13,7 +13,8 @@ from IPython.core import magic_arguments, page from IPython.core.error import UsageError from IPython.core.magic import Magics, magics_class, line_magic, magic_escapes -from IPython.utils.text import format_screen, dedent, indent +import inspect +from IPython.utils.text import format_screen, indent from IPython.testing.skipdoctest import skip_doctest from IPython.utils.ipstruct import Struct @@ -197,11 +198,11 @@ def _magic_docs(self, brief=False, rest=False): return ''.join( [format_string % (magic_escapes['line'], fname, - indent(dedent(fndoc))) + indent(inspect.cleandoc(fndoc))) for fname, fndoc in sorted(docs['line'].items())] + [format_string % (magic_escapes['cell'], fname, - indent(dedent(fndoc))) + indent(inspect.cleandoc(fndoc))) for fname, fndoc in sorted(docs['cell'].items())] ) diff --git a/IPython/utils/text.py b/IPython/utils/text.py index cd907f7782..488f491d51 100644 --- a/IPython/utils/text.py +++ b/IPython/utils/text.py @@ -177,7 +177,7 @@ def match_target(s: str) -> str: return "" if isinstance(pattern, str): - pred = lambda x : re.search(pattern, x, re.IGNORECASE) + pred = lambda x: re.search(pattern, x, re.IGNORECASE) else: pred = pattern if not prune: @@ -238,9 +238,9 @@ def sort( # type:ignore[override] #decorate, sort, undecorate if field is not None: - dsu = [[SList([line]).fields(field), line] for line in self] + dsu = [[SList([line]).fields(field), line] for line in self] else: - dsu = [[line, line] for line in self] + dsu = [[line, line] for line in self] if nums: for i in range(len(dsu)): numstr = "".join([ch for ch in dsu[i][0] if ch.isdigit()]) @@ -250,7 +250,6 @@ def sort( # type:ignore[override] n = 0 dsu[i][0] = n - dsu.sort() return type(self)([t[1] for t in dsu]) @@ -334,7 +333,8 @@ def marquee(txt: str = "", width: int = 78, mark: str = "*") -> str: if not txt: return (mark*width)[:width] nmark = (width-len(txt)-2)//len(mark)//2 - if nmark < 0: nmark =0 + if nmark < 0: + nmark =0 marks = mark*nmark return '%s %s %s' % (marks,txt,marks) @@ -348,30 +348,23 @@ def format_screen(strng: str) -> str: strng = par_re.sub('',strng) return strng - def dedent(text: str) -> str: """Equivalent of textwrap.dedent that ignores unindented first line. - This means it will still dedent strings like: - '''foo - is a bar - ''' - - For use in wrap_paragraphs. + .. deprecated:: + Use `inspect.cleandoc` instead. This function will be removed in a future version. """ - + warnings.warn( + "IPython.utils.text.dedent is deprecated. Use inspect.cleandoc instead.", + DeprecationWarning, + stacklevel=2, + ) if text.startswith('\n'): - # text starts with blank line, don't ignore the first line return textwrap.dedent(text) - - # split first line - splits = text.split('\n',1) + splits = text.split('\n', 1) if len(splits) == 1: - # only one line return textwrap.dedent(text) - first, rest = splits - # dedent everything but the first line rest = textwrap.dedent(rest) return '\n'.join([first, rest]) @@ -462,13 +455,13 @@ def get_field(self, name: str, args: Any, kwargs: Any) -> Tuple[Any, str]: class FullEvalFormatter(Formatter): """A String Formatter that allows evaluation of simple expressions. - + Any time a format key is not found in the kwargs, it will be tried as an expression in the kwargs namespace. - + Note that this version allows slicing using [1:2], so you cannot specify a format string. Use :class:`EvalFormatter` to permit format strings. - + Examples -------- :: @@ -560,7 +553,7 @@ def parse(self, fmt_string: str) -> Iterator[Tuple[Any, Any, Any, Any]]: yield (txt + new_txt, new_field, "", None) txt = "" continue_from = m.end() - + # Re-yield the {foo} style pattern yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion) @@ -609,7 +602,7 @@ def _get_or_default(mylist: List[T], i: int, default: T) -> T: """return list item number, or default if don't exist""" if i >= len(mylist): return default - else : + else: return mylist[i] diff --git a/tests/test_history.py b/tests/test_history.py index afe625682b..d6b1d9d23d 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -265,13 +265,21 @@ def test_hist_file_config(hmmax3): assert hm.hist_file == cfg.HistoryManager.hist_file finally: try: - Path(tfile.name).unlink() - except OSError: - # same catch as in testing.tools.TempFileMixin - # On Windows, even though we close the file, we still can't - # delete it. I have no clue why + hm.end_session() + hm.save_thread.stop() + hm.db.close() + except Exception: pass - HistoryManager.__max_inst = 1 + + try: + Path(tfile.name).unlink() + except OSError: + # same catch as in testing.tools.TempFileMixin + # On Windows, even though we close the file, we still can't + # delete it. I have no clue why + pass + + HistoryManager._max_inst = 1 def test_histmanager_disabled(hmmax2): @@ -386,7 +394,7 @@ def test_calling_run_cell(hmmax2): try: ip.history_manager = HistoryManager(shell=ip, hist_file=hist_file) import time - + session_number = ip.history_manager.session_number ip.run_cell(raw_cell="get_ipython().run_cell(raw_cell='1', store_history=True)", store_history=True) while ip.history_manager.db_input_cache: @@ -400,4 +408,4 @@ def test_calling_run_cell(hmmax2): ip.history_manager.db.close() ip.history_manager = hist_manager_ori - assert session_number == new_session_number, ValueError(f"{session_number} != {new_session_number}") + assert session_number == new_session_number, ValueError(f"{session_number} != {new_session_number}") \ No newline at end of file