diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000000..38b6ab9ec5d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,26 @@ +name: Build docs + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install Graphviz + run: | + sudo apt-get update + sudo apt-get install graphviz + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip setuptools + pip install -r docs/requirements.txt + - name: Build docs + run: | + python tools/fixup_whats_new_pr.py + make -C docs/ html SPHINXOPTS="-W" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000000..f5840135a29 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Run tests + +on: + push: + pull_request: + # Run weekly on Monday at 1:23 UTC + schedule: + - cron: '23 1 * * 1' + workflow_dispatch: + + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.7", "3.8", "3.9", "3.10"] + # Test all on ubuntu, test ends on macos + include: + - os: macos-latest + python-version: "3.7" + - os: macos-latest + python-version: "3.9" + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install and update Python dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade -e file://$PWD#egg=ipython[test] + python -m pip install --upgrade --upgrade-strategy eager trio curio + python -m pip install --upgrade pytest pytest-trio 'matplotlib!=3.2.0' + python -m pip install --upgrade check-manifest pytest-cov anyio + - name: Check manifest + run: check-manifest + - name: iptest + if: matrix.python-version != '3.10' + run: | + cd /tmp && iptest --coverage xml && cd - + cp /tmp/ipy_coverage.xml ./ + cp /tmp/.coverage ./ + - name: pytest + run: | + pytest + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 diff --git a/.gitignore b/.gitignore index 1fc0e22a320..e918d3eec5f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,3 @@ __pycache__ .pytest_cache .python-version venv*/ -.idea/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 34d0cd10083..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,123 +0,0 @@ -# http://travis-ci.org/#!/ipython/ipython -language: python -os: linux - -addons: - apt: - packages: - - graphviz - -python: - - 3.6 - -sudo: false - -env: - global: - - PATH=$TRAVIS_BUILD_DIR/pandoc:$PATH - -group: edge - -before_install: - - | - # install Python on macOS - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then - env | sort - if ! which python$TRAVIS_PYTHON_VERSION; then - HOMEBREW_NO_AUTO_UPDATE=1 brew tap minrk/homebrew-python-frameworks - HOMEBREW_NO_AUTO_UPDATE=1 brew cask install python-framework-${TRAVIS_PYTHON_VERSION/./} - fi - python3 -m pip install virtualenv - python3 -m virtualenv -p $(which python$TRAVIS_PYTHON_VERSION) ~/travis-env - source ~/travis-env/bin/activate - fi - - python --version - -install: - - pip install pip --upgrade - - pip install setuptools --upgrade - - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then - echo "for the time being still test on 3.6"; - sed -ibkp s/7/6/g setup.py; - git diff; - fi - - pip install -e file://$PWD#egg=ipython[test] --upgrade - - pip install trio curio --upgrade --upgrade-strategy eager - - pip install 'pytest<6' 'matplotlib !=3.2.0' mypy - - pip install codecov check-manifest --upgrade - -script: - - check-manifest - - | - if [[ "$TRAVIS_PYTHON_VERSION" == "nightly" ]]; then - # on nightly fake parso known the grammar - cp /home/travis/virtualenv/python3.9-dev/lib/python3.9/site-packages/parso/python/grammar38.txt /home/travis/virtualenv/python3.9-dev/lib/python3.9/site-packages/parso/python/grammar39.txt - fi - - cd /tmp && iptest --coverage xml && cd - - - pytest IPython - - mypy --ignore-missing-imports -m IPython.terminal.ptutils - # On the latest Python (on Linux) only, make sure that the docs build. - - | - if [[ "$TRAVIS_PYTHON_VERSION" == "3.7" ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - pip install -r docs/requirements.txt - python tools/fixup_whats_new_pr.py - make -C docs/ html SPHINXOPTS="-W" - fi - -after_success: - - cp /tmp/ipy_coverage.xml ./ - - cp /tmp/.coverage ./ - - codecov - -matrix: - include: - - arch: amd64 - python: "3.7" - dist: xenial - sudo: true - - arch: amd64 - python: "3.8-dev" - dist: xenial - sudo: true - - arch: amd64 - python: "3.7-dev" - dist: xenial - sudo: true - - arch: amd64 - python: "nightly" - dist: xenial - sudo: true - - arch: arm64 - python: "nightly" - dist: bionic - env: ARM64=True - sudo: true - - os: osx - language: generic - python: 3.6 - env: TRAVIS_PYTHON_VERSION=3.6 - - os: osx - language: generic - python: 3.7 - env: TRAVIS_PYTHON_VERSION=3.7 - allow_failures: - - python: nightly - -before_deploy: - - rm -rf dist/ - - python setup.py sdist - - python setup.py bdist_wheel - -deploy: - provider: releases - api_key: - secure: Y/Ae9tYs5aoBU8bDjN2YrwGG6tCbezj/h3Lcmtx8HQavSbBgXnhnZVRb2snOKD7auqnqjfT/7QMm4ZyKvaOEgyggGktKqEKYHC8KOZ7yp8I5/UMDtk6j9TnXpSqqBxPiud4MDV76SfRYEQiaDoG4tGGvSfPJ9KcNjKrNvSyyxns= - file: dist/* - file_glob: true - skip_cleanup: true - on: - repo: ipython/ipython - all_branches: true # Backports are released from e.g. 5.x branch - tags: true - python: 3.6 # Any version should work, but we only need one - condition: $TRAVIS_OS_NAME = "linux" diff --git a/IPython/__init__.py b/IPython/__init__.py index 4fb77107680..c17ec76a602 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -65,6 +65,10 @@ __license__ = release.license __version__ = release.version version_info = release.version_info +# list of CVEs that should have been patched in this release. +# this is informational and should not be relied upon. +__patched_cves__ = {"CVE-2022-21699"} + def embed_kernel(module=None, local_ns=None, **kwargs): """Embed and start an IPython kernel in a given scope. diff --git a/IPython/core/application.py b/IPython/core/application.py index 93639d88e2c..b319888b59b 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -133,7 +133,7 @@ def _config_file_name_changed(self, change): config_file_paths = List(Unicode()) @default('config_file_paths') def _config_file_paths_default(self): - return [os.getcwd()] + return [] extra_config_file = Unicode( help="""Path to an extra config file to load. @@ -374,7 +374,7 @@ def init_profile_dir(self): self.log.fatal("Profile %r not found."%self.profile) self.exit(1) else: - self.log.debug("Using existing profile dir: %r"%p.location) + self.log.debug(f"Using existing profile dir: {p.location!r}") else: location = self.config.ProfileDir.location # location is fully specified @@ -394,7 +394,7 @@ def init_profile_dir(self): self.log.fatal("Profile directory %r not found."%location) self.exit(1) else: - self.log.info("Using existing profile dir: %r"%location) + self.log.debug(f"Using existing profile dir: {p.location!r}") # if profile_dir is specified explicitly, set profile name dir_name = os.path.basename(p.location) if dir_name.startswith('profile_'): diff --git a/IPython/core/async_helpers.py b/IPython/core/async_helpers.py index fb4cc193250..fca78def85a 100644 --- a/IPython/core/async_helpers.py +++ b/IPython/core/async_helpers.py @@ -13,19 +13,29 @@ import ast import sys +import asyncio import inspect from textwrap import dedent, indent class _AsyncIORunner: + def __init__(self): + self._loop = None + + @property + def loop(self): + """Always returns a non-closed event loop""" + if self._loop is None or self._loop.is_closed(): + policy = asyncio.get_event_loop_policy() + self._loop = policy.new_event_loop() + policy.set_event_loop(self._loop) + return self._loop def __call__(self, coro): """ Handler for asyncio autoawait """ - import asyncio - - return asyncio.get_event_loop().run_until_complete(coro) + return self.loop.run_until_complete(coro) def __str__(self): return 'asyncio' diff --git a/IPython/core/compilerop.py b/IPython/core/compilerop.py index c4771af7303..50672a19541 100644 --- a/IPython/core/compilerop.py +++ b/IPython/core/compilerop.py @@ -99,7 +99,7 @@ def ast_parse(self, source, filename='', symbol='exec'): Arguments are exactly the same as ast.parse (in the standard library), and are passed to the built-in compile function.""" return compile(source, filename, symbol, self.flags | PyCF_ONLY_AST, 1) - + def reset_compiler_flags(self): """Reset compiler flags to default state.""" # This value is copied from codeop.Compile.__init__, so if that ever @@ -112,25 +112,53 @@ def compiler_flags(self): """ return self.flags - def cache(self, code, number=0): + def get_code_name(self, raw_code, transformed_code, number): + """Compute filename given the code, and the cell number. + + Parameters + ---------- + raw_code : str + The raw cell code. + transformed_code : str + The executable Python source code to cache and compile. + number : int + A number which forms part of the code's name. Used for the execution + counter. + + Returns + ------- + The computed filename. + """ + return code_name(transformed_code, number) + + def cache(self, transformed_code, number=0, raw_code=None): """Make a name for a block of code, and cache the code. Parameters ---------- - code : str - The Python source code to cache. + transformed_code : str + The executable Python source code to cache and compile. number : int A number which forms part of the code's name. Used for the execution counter. + raw_code : str + The raw code before transformation, if None, set to `transformed_code`. Returns ------- The name of the cached code (as a string). Pass this as the filename argument to compilation, so that tracebacks are correctly hooked up. """ - name = code_name(code, number) - entry = (len(code), time.time(), - [line+'\n' for line in code.splitlines()], name) + if raw_code is None: + raw_code = transformed_code + + name = self.get_code_name(raw_code, transformed_code, number) + entry = ( + len(transformed_code), + time.time(), + [line + "\n" for line in transformed_code.splitlines()], + name, + ) linecache.cache[name] = entry linecache._ipython_cache[name] = entry return name @@ -146,7 +174,7 @@ def extra_flags(self, flags): yield finally: # turn off only the bits we turned on so that something like - # __future__ that set flags stays. + # __future__ that set flags stays. self.flags &= ~turn_on_bits diff --git a/IPython/core/completer.py b/IPython/core/completer.py index bc114f0f66b..9a84f7e5584 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -41,7 +41,7 @@ Only valid Python identifiers will complete. Combining characters (like arrow or dots) are also available, unlike latex they need to be put after the their -counterpart that is to say, `F\\\\vec` is correct, not `\\\\vecF`. +counterpart that is to say, ``F\\\\vec`` is correct, not ``\\\\vecF``. Some browsers are known to display combining characters incorrectly. @@ -110,26 +110,23 @@ # Copyright (C) 2001 Python Software Foundation, www.python.org -import __main__ import builtins as builtin_mod import glob -import time import inspect import itertools import keyword import os import re +import string import sys +import time import unicodedata -import string import warnings - from contextlib import contextmanager from importlib import import_module -from typing import Iterator, List, Tuple, Iterable from types import SimpleNamespace +from typing import Iterable, Iterator, List, Tuple -from traitlets.config.configurable import Configurable from IPython.core.error import TryNext from IPython.core.inputtransformer2 import ESC_MAGIC from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol @@ -137,7 +134,10 @@ from IPython.utils import generics from IPython.utils.dir2 import dir2, get_real_method from IPython.utils.process import arg_split -from traitlets import Bool, Enum, observe, Int +from traitlets import Bool, Enum, Int, observe +from traitlets.config.configurable import Configurable + +import __main__ # skip module docstests skip_doctest = True @@ -166,7 +166,13 @@ # may have trouble processing. MATCHES_LIMIT = 500 -_deprecation_readline_sentinel = object() + +class Sentinel: + def __repr__(self): + return "" + + +_deprecation_readline_sentinel = Sentinel() class ProvisionalCompleterWarning(FutureWarning): @@ -193,7 +199,9 @@ def provisionalcompleter(action='ignore'): >>> completer.do_experimental_things() # raises. - .. note:: Unstable + .. note:: + + Unstable By using this context manager you agree that the API in use may change without warning, and that you won't complain if they do so. @@ -348,7 +356,9 @@ class Completion: """ Completion object used and return by IPython completers. - .. warning:: Unstable + .. warning:: + + Unstable This function is unstable, API may change without warning. It will also raise unless use in proper context manager. @@ -411,7 +421,9 @@ def _deduplicate_completions(text: str, completions: _IC)-> _IC: """ Deduplicate a set of completions. - .. warning:: Unstable + .. warning:: + + Unstable This function is unstable, API may change without warning. @@ -454,7 +466,9 @@ def rectify_completions(text: str, completions: _IC, *, _debug=False)->_IC: """ Rectify a set of completions to all have the same ``start`` and ``end`` - .. warning:: Unstable + .. warning:: + + Unstable This function is unstable, API may change without warning. It will also raise unless use in proper context manager. @@ -988,7 +1002,17 @@ def _make_signature(completion)-> str: """ - return '(%s)'% ', '.join([f for f in (_formatparamchildren(p) for p in completion.params) if f]) + # it looks like this might work on jedi 0.17 + if hasattr(completion, 'get_signatures'): + signatures = completion.get_signatures() + if not signatures: + return '(?)' + + c0 = completion.get_signatures()[0] + return '('+c0.to_string().split('(', maxsplit=1)[1] + + return '(%s)'% ', '.join([f for f in (_formatparamchildren(p) for signature in completion.get_signatures() + for p in signature.defined_names()) if f]) class IPCompleter(Completer): """Extension of the completer class with IPython-specific features""" @@ -1125,18 +1149,18 @@ def matchers(self): if self.use_jedi: return [ *self.custom_matchers, + self.dict_key_matches, self.file_matches, self.magic_matches, - self.dict_key_matches, ] else: return [ *self.custom_matchers, + self.dict_key_matches, self.python_matches, self.file_matches, self.magic_matches, self.python_func_kw_matches, - self.dict_key_matches, ] def all_completions(self, text) -> List[str]: @@ -1370,8 +1394,7 @@ def _jedi_matches(self, cursor_column:int, cursor_line:int, text:str): else: raise ValueError("Don't understand self.omit__names == {}".format(self.omit__names)) - interpreter = jedi.Interpreter( - text[:offset], namespaces, column=cursor_column, line=cursor_line + 1) + interpreter = jedi.Interpreter(text[:offset], namespaces) try_jedi = True try: @@ -1398,7 +1421,7 @@ def _jedi_matches(self, cursor_column:int, cursor_line:int, text:str): if not try_jedi: return [] try: - return filter(completion_filter, interpreter.completions()) + return filter(completion_filter, interpreter.complete(column=cursor_column, line=cursor_line + 1)) except Exception as e: if self.debug: return [_FakeJediCompletion('Oops Jedi has crashed, please report a bug with the following:\n"""\n%s\ns"""' % (e))] @@ -1479,7 +1502,7 @@ def _default_arguments(self, obj): inspect.Parameter.POSITIONAL_OR_KEYWORD) try: - sig = inspect.signature(call_obj) + sig = inspect.signature(obj) ret.extend(k for k, v in sig.parameters.items() if v.kind in _keeps) except ValueError: @@ -1767,7 +1790,9 @@ def completions(self, text: str, offset: int)->Iterator[Completion]: """ Returns an iterator over the possible completions - .. warning:: Unstable + .. warning:: + + Unstable This function is unstable, API may change without warning. It will also raise unless use in proper context manager. diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index bf0a8eff3c3..1744bdb8a8e 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -2,6 +2,76 @@ """ Pdb debugger class. + +This is an extension to PDB which adds a number of new features. +Note that there is also the `IPython.terminal.debugger` class which provides UI +improvements. + +We also strongly recommend to use this via the `ipdb` package, which provides +extra configuration options. + +Among other things, this subclass of PDB: + - supports many IPython magics like pdef/psource + - hide frames in tracebacks based on `__tracebackhide__` + - allows to skip frames based on `__debuggerskip__` + +The skipping and hiding frames are configurable via the `skip_predicates` +command. + +By default, frames from readonly files will be hidden, frames containing +``__tracebackhide__=True`` will be hidden. + +Frames containing ``__debuggerskip__`` will be stepped over, frames who's parent +frames value of ``__debuggerskip__`` is ``True`` will be skipped. + + >>> def helpers_helper(): + ... pass + ... + ... def helper_1(): + ... print("don't step in me") + ... helpers_helpers() # will be stepped over unless breakpoint set. + ... + ... + ... def helper_2(): + ... print("in me neither") + ... + +One can define a decorator that wraps a function between the two helpers: + + >>> def pdb_skipped_decorator(function): + ... + ... + ... def wrapped_fn(*args, **kwargs): + ... __debuggerskip__ = True + ... helper_1() + ... __debuggerskip__ = False + ... result = function(*args, **kwargs) + ... __debuggerskip__ = True + ... helper_2() + ... # setting __debuggerskip__ to False again is not necessary + ... return result + ... + ... return wrapped_fn + +When decorating a function, ipdb will directly step into ``bar()`` by +default: + + >>> @foo_decorator + ... def bar(x, y): + ... return x * y + + +You can toggle the behavior with + + ipdb> skip_predicates debuggerskip false + +or configure it in your ``.pdbrc`` + + + +Licencse +-------- + Modified from the standard pdb.Pdb class to avoid including readline, so that the command line completion of other programs which include this isn't damaged. @@ -9,11 +79,16 @@ In the future, this class will be expanded with improvements over the standard pdb. -The code in this file is mainly lifted out of cmd.py in Python 2.2, with minor -changes. Licensing should therefore be under the standard Python terms. For -details on the PSF (Python Software Foundation) standard license, see: +The original code in this file is mainly lifted out of cmd.py in Python 2.2, +with minor changes. Licensing should therefore be under the standard Python +terms. For details on the PSF (Python Software Foundation) standard license, +see: https://docs.python.org/2/license.html + + +All the changes since then are under the same license as IPython. + """ #***************************************************************************** @@ -33,6 +108,7 @@ import sys import warnings import re +import os from IPython import get_ipython from IPython.utils import PyColorize @@ -50,6 +126,9 @@ # it does so with some limitations. The rest of this support is implemented in # the Tracer constructor. +DEBUGGERSKIP = "__debuggerskip__" + + def make_arrow(pad): """generate the leading arrow in front of traceback or debugger""" if pad >= 2: @@ -198,21 +277,44 @@ class Pdb(OldPdb): for a standalone version that uses prompt_toolkit, see `IPython.terminal.debugger.TerminalPdb` and `IPython.terminal.debugger.set_trace()` + + + This debugger can hide and skip frames that are tagged according to some predicates. + See the `skip_predicates` commands. + """ + default_predicates = { + "tbhide": True, + "readonly": False, + "ipython_internal": True, + "debuggerskip": True, + } + def __init__(self, color_scheme=None, completekey=None, stdin=None, stdout=None, context=5, **kwargs): """Create a new IPython debugger. - - :param color_scheme: Deprecated, do not use. - :param completekey: Passed to pdb.Pdb. - :param stdin: Passed to pdb.Pdb. - :param stdout: Passed to pdb.Pdb. - :param context: Number of lines of source code context to show when + + Parameters + ---------- + color_scheme : default None + Deprecated, do not use. + completekey : default None + Passed to pdb.Pdb. + stdin : default None + Passed to pdb.Pdb. + stdout : default None + Passed to pdb.Pdb. + context : int + Number of lines of source code context to show when displaying stacktrace information. - :param kwargs: Passed to pdb.Pdb. - The possibilities are python version dependent, see the python - docs for more info. + **kwargs + Passed to pdb.Pdb. + + Notes + ----- + The possibilities are python version dependent, see the python + docs for more info. """ # Parent constructor: @@ -281,12 +383,43 @@ def __init__(self, color_scheme=None, completekey=None, # Set the prompt - the default prompt is '(Pdb)' self.prompt = prompt self.skip_hidden = True + self.report_skipped = True + + # list of predicates we use to skip frames + self._predicates = self.default_predicates + # def set_colors(self, scheme): """Shorthand access to the color table scheme selector method.""" self.color_scheme_table.set_active_scheme(scheme) self.parser.style = scheme + def set_trace(self, frame=None): + if frame is None: + frame = sys._getframe().f_back + self.initial_frame = frame + return super().set_trace(frame) + + def _hidden_predicate(self, frame): + """ + Given a frame return whether it it should be hidden or not by IPython. + """ + + if self._predicates["readonly"]: + fname = frame.f_code.co_filename + # we need to check for file existence and interactively define + # function would otherwise appear as RO. + if os.path.isfile(fname) and not os.access(fname, os.W_OK): + return True + + if self._predicates["tbhide"]: + if frame in (self.curframe, getattr(self, "initial_frame", None)): + return False + frame_locals = self._get_frame_locals(frame) + if "__tracebackhide__" not in frame_locals: + return False + return frame_locals["__tracebackhide__"] + return False def hidden_frames(self, stack): """ @@ -298,12 +431,9 @@ def hidden_frames(self, stack): # locals whenever the .f_locals accessor is called, so we # avoid calling it here to preserve self.curframe_locals. # Futhermore, there is no good reason to hide the current frame. - ip_hide = [ - False if s[0] is self.curframe else s[0].f_locals.get( - "__tracebackhide__", False) - for s in stack] + ip_hide = [self._hidden_predicate(s[0]) for s in stack] ip_start = [i for i, s in enumerate(ip_hide) if s == "__ipython_bottom__"] - if ip_start: + if ip_start and self._predicates["ipython_internal"]: ip_hide = [h if i > ip_start[0] else True for (i, h) in enumerate(ip_hide)] return ip_hide @@ -379,6 +509,28 @@ def print_stack_entry(self, frame_lineno, prompt_prefix='\n-> ', self.shell.hooks.synchronize_with_editor(filename, lineno, 0) # vds: << + def _get_frame_locals(self, frame): + """ " + Acessing f_local of current frame reset the namespace, so we want to avoid + that or the following can happend + + ipdb> foo + "old" + ipdb> foo = "new" + ipdb> foo + "new" + ipdb> where + ipdb> foo + "old" + + So if frame is self.current_frame we instead return self.curframe_locals + + """ + if frame is self.curframe: + return self.curframe_locals + else: + return frame.f_locals + def format_stack_entry(self, frame_lineno, lprefix=': ', context=None): if context is None: context = self.context @@ -406,10 +558,11 @@ def format_stack_entry(self, frame_lineno, lprefix=': ', context=None): frame, lineno = frame_lineno return_value = '' - if '__return__' in frame.f_locals: - rv = frame.f_locals['__return__'] - #return_value += '->' - return_value += reprlib.repr(rv) + '\n' + loc_frame = self._get_frame_locals(frame) + if "__return__" in loc_frame: + rv = loc_frame["__return__"] + # return_value += '->' + return_value += reprlib.repr(rv) + "\n" ret.append(return_value) #s = filename + '(' + `lineno` + ')' @@ -421,10 +574,10 @@ def format_stack_entry(self, frame_lineno, lprefix=': ', context=None): else: func = "" - call = '' - if func != '?': - if '__args__' in frame.f_locals: - args = reprlib.repr(frame.f_locals['__args__']) + call = "" + if func != "?": + if "__args__" in loc_frame: + args = reprlib.repr(loc_frame["__args__"]) else: args = '()' call = tpl_call % (func, args) @@ -514,15 +667,68 @@ def print_list_lines(self, filename, first, last): except KeyboardInterrupt: pass + def do_skip_predicates(self, args): + """ + Turn on/off individual predicates as to whether a frame should be hidden/skip. + + The global option to skip (or not) hidden frames is set with skip_hidden + + To change the value of a predicate + + skip_predicates key [true|false] + + Call without arguments to see the current values. + + To permanently change the value of an option add the corresponding + command to your ``~/.pdbrc`` file. If you are programmatically using the + Pdb instance you can also change the ``default_predicates`` class + attribute. + """ + if not args.strip(): + print("current predicates:") + for (p, v) in self._predicates.items(): + print(" ", p, ":", v) + return + type_value = args.strip().split(" ") + if len(type_value) != 2: + print( + f"Usage: skip_predicates , with one of {set(self._predicates.keys())}" + ) + return + + type_, value = type_value + if type_ not in self._predicates: + print(f"{type_!r} not in {set(self._predicates.keys())}") + return + if value.lower() not in ("true", "yes", "1", "no", "false", "0"): + print( + f"{value!r} is invalid - use one of ('true', 'yes', '1', 'no', 'false', '0')" + ) + return + + self._predicates[type_] = value.lower() in ("true", "yes", "1") + if not any(self._predicates.values()): + print( + "Warning, all predicates set to False, skip_hidden may not have any effects." + ) + def do_skip_hidden(self, arg): """ Change whether or not we should skip frames with the __tracebackhide__ attribute. """ - if arg.strip().lower() in ("true", "yes"): + if not arg.strip(): + print( + f"skip_hidden = {self.skip_hidden}, use 'yes','no', 'true', or 'false' to change." + ) + elif arg.strip().lower() in ("true", "yes"): self.skip_hidden = True elif arg.strip().lower() in ("false", "no"): self.skip_hidden = False + if not any(self._predicates.values()): + print( + "Warning, all predicates set to False, skip_hidden may not have any effects." + ) def do_list(self, arg): """Print lines of code from the current stack frame @@ -562,7 +768,7 @@ def do_list(self, arg): def getsourcelines(self, obj): lines, lineno = inspect.findsource(obj) - if inspect.isframe(obj) and obj.f_globals is obj.f_locals: + if inspect.isframe(obj) and obj.f_globals is self._get_frame_locals(obj): # must be a module frame: do not try to cut a block out of it return lines, 1 elif inspect.ismodule(obj): @@ -590,6 +796,7 @@ def do_debug(self, arg): argument (which is an arbitrary expression or statement to be executed in the current environment). """ + trace_function = sys.gettrace() sys.settrace(None) globals = self.curframe.f_globals locals = self.curframe_locals @@ -600,55 +807,67 @@ def do_debug(self, arg): self.message("ENTERING RECURSIVE DEBUGGER") sys.call_tracing(p.run, (arg, globals, locals)) self.message("LEAVING RECURSIVE DEBUGGER") - sys.settrace(self.trace_dispatch) + sys.settrace(trace_function) self.lastcmd = p.lastcmd def do_pdef(self, arg): """Print the call signature for any callable object. The debugger interface to %pdef""" - namespaces = [('Locals', self.curframe.f_locals), - ('Globals', self.curframe.f_globals)] - self.shell.find_line_magic('pdef')(arg, namespaces=namespaces) + namespaces = [ + ("Locals", self.curframe_locals), + ("Globals", self.curframe.f_globals), + ] + self.shell.find_line_magic("pdef")(arg, namespaces=namespaces) def do_pdoc(self, arg): """Print the docstring for an object. The debugger interface to %pdoc.""" - namespaces = [('Locals', self.curframe.f_locals), - ('Globals', self.curframe.f_globals)] - self.shell.find_line_magic('pdoc')(arg, namespaces=namespaces) + namespaces = [ + ("Locals", self.curframe_locals), + ("Globals", self.curframe.f_globals), + ] + self.shell.find_line_magic("pdoc")(arg, namespaces=namespaces) def do_pfile(self, arg): """Print (or run through pager) the file where an object is defined. The debugger interface to %pfile. """ - namespaces = [('Locals', self.curframe.f_locals), - ('Globals', self.curframe.f_globals)] - self.shell.find_line_magic('pfile')(arg, namespaces=namespaces) + namespaces = [ + ("Locals", self.curframe_locals), + ("Globals", self.curframe.f_globals), + ] + self.shell.find_line_magic("pfile")(arg, namespaces=namespaces) def do_pinfo(self, arg): """Provide detailed information about an object. The debugger interface to %pinfo, i.e., obj?.""" - namespaces = [('Locals', self.curframe.f_locals), - ('Globals', self.curframe.f_globals)] - self.shell.find_line_magic('pinfo')(arg, namespaces=namespaces) + namespaces = [ + ("Locals", self.curframe_locals), + ("Globals", self.curframe.f_globals), + ] + self.shell.find_line_magic("pinfo")(arg, namespaces=namespaces) def do_pinfo2(self, arg): """Provide extra detailed information about an object. The debugger interface to %pinfo2, i.e., obj??.""" - namespaces = [('Locals', self.curframe.f_locals), - ('Globals', self.curframe.f_globals)] - self.shell.find_line_magic('pinfo2')(arg, namespaces=namespaces) + namespaces = [ + ("Locals", self.curframe_locals), + ("Globals", self.curframe.f_globals), + ] + self.shell.find_line_magic("pinfo2")(arg, namespaces=namespaces) def do_psource(self, arg): """Print (or run through pager) the source code for an object.""" - namespaces = [('Locals', self.curframe.f_locals), - ('Globals', self.curframe.f_globals)] - self.shell.find_line_magic('psource')(arg, namespaces=namespaces) + namespaces = [ + ("Locals", self.curframe_locals), + ("Globals", self.curframe.f_globals), + ] + self.shell.find_line_magic("psource")(arg, namespaces=namespaces) def do_where(self, arg): """w(here) @@ -670,16 +889,69 @@ def do_where(self, arg): do_w = do_where + def break_anywhere(self, frame): + """ + + _stop_in_decorator_internals is overly restrictive, as we may still want + to trace function calls, so we need to also update break_anywhere so + that is we don't `stop_here`, because of debugger skip, we may still + stop at any point inside the function + + """ + + sup = super().break_anywhere(frame) + if sup: + return sup + if self._predicates["debuggerskip"]: + if DEBUGGERSKIP in frame.f_code.co_varnames: + return True + if frame.f_back and self._get_frame_locals(frame.f_back).get(DEBUGGERSKIP): + return True + return False + + @skip_doctest + def _is_in_decorator_internal_and_should_skip(self, frame): + """ + Utility to tell us whether we are in a decorator internal and should stop. + + + + """ + + # if we are disabled don't skip + if not self._predicates["debuggerskip"]: + return False + + # if frame is tagged, skip by default. + if DEBUGGERSKIP in frame.f_code.co_varnames: + return True + + # if one of the parent frame value set to True skip as well. + + cframe = frame + while getattr(cframe, "f_back", None): + cframe = cframe.f_back + if self._get_frame_locals(cframe).get(DEBUGGERSKIP): + return True + + return False + def stop_here(self, frame): """Check if pdb should stop here""" if not super().stop_here(frame): return False - if self.skip_hidden and frame.f_locals.get("__tracebackhide__", False): - if self._wait_for_mainpyfile: - return False - Colors = self.color_scheme_table.active_colors - ColorsNormal = Colors.Normal - print(f"{Colors.excName} [... skipped 1 hidden frame]{ColorsNormal}\n") + + if self._is_in_decorator_internal_and_should_skip(frame) is True: + return False + + hidden = False + if self.skip_hidden: + hidden = self._hidden_predicate(frame) + if hidden: + if self.report_skipped: + Colors = self.color_scheme_table.active_colors + ColorsNormal = Colors.Normal + print(f"{Colors.excName} [... skipped 1 hidden frame]{ColorsNormal}\n") return False return True @@ -690,8 +962,8 @@ def do_up(self, arg): Will skip hidden frames. """ - ## modified version of upstream that skips - # frames with __tracebackide__ + # modified version of upstream that skips + # frames with __tracebackhide__ if self.curindex == 0: self.error("Oldest frame") return @@ -776,13 +1048,27 @@ def do_down(self, arg): do_d = do_down do_u = do_up + def do_context(self, context): + """context number_of_lines + Set the number of lines of source code to show when displaying + stacktrace information. + """ + try: + new_context = int(context) + if new_context <= 0: + raise ValueError() + self.context = new_context + except ValueError: + self.error("The 'context' command requires a positive integer argument.") + + class InterruptiblePdb(Pdb): """Version of debugger where KeyboardInterrupt exits the debugger altogether.""" - def cmdloop(self): + def cmdloop(self, intro=None): """Wrap cmdloop() such that KeyboardInterrupt stops the debugger.""" try: - return OldPdb.cmdloop(self) + return OldPdb.cmdloop(self, intro=intro) except KeyboardInterrupt: self.stop_here = lambda frame: False self.do_quit("") diff --git a/IPython/core/display.py b/IPython/core/display.py index 424414a662f..f45e7599c9e 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -57,7 +57,7 @@ def _display_mimetype(mimetype, objs, raw=False, metadata=None): ---------- mimetype : str The mimetype to be published (e.g. 'image/png') - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw text data to display. raw : bool @@ -139,7 +139,7 @@ def display(*objs, include=None, exclude=None, metadata=None, transient=None, di Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display. raw : bool, optional Are the objects to be displayed already mimetype-keyed dicts of raw display data, @@ -163,6 +163,9 @@ def display(*objs, include=None, exclude=None, metadata=None, transient=None, di Set an id for the display. This id can be used for updating this display area later via update_display. If given as `True`, generate a new `display_id` + clear : bool, optional + Should the output area be cleared before displaying anything? If True, + this will wait for additional output before clearing. [default: False] kwargs: additional keyword-args, optional Additional keyword-arguments are passed through to the display publisher. @@ -281,8 +284,9 @@ def display(*objs, include=None, exclude=None, metadata=None, transient=None, di # Directly print objects. print(*objs) return - - raw = kwargs.pop('raw', False) + + raw = kwargs.pop("raw", False) + clear = kwargs.pop("clear", False) if transient is None: transient = {} if metadata is None: @@ -306,6 +310,9 @@ def display(*objs, include=None, exclude=None, metadata=None, transient=None, di if not raw: format = InteractiveShell.instance().display_formatter.format + if clear: + clear_output(wait=True) + for obj in objs: if raw: publish_display_data(data=obj, metadata=metadata, **kwargs) @@ -398,7 +405,7 @@ def display_pretty(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw text data to display. raw : bool @@ -418,7 +425,7 @@ def display_html(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw HTML data to display. raw : bool @@ -435,7 +442,7 @@ def display_markdown(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw markdown data to display. raw : bool @@ -453,7 +460,7 @@ def display_svg(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw svg data to display. raw : bool @@ -470,7 +477,7 @@ def display_png(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw png data to display. raw : bool @@ -487,7 +494,7 @@ def display_jpeg(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw JPEG data to display. raw : bool @@ -504,7 +511,7 @@ def display_latex(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw latex data to display. raw : bool @@ -523,7 +530,7 @@ def display_json(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw json data to display. raw : bool @@ -540,7 +547,7 @@ def display_javascript(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw javascript data to display. raw : bool @@ -557,7 +564,7 @@ def display_pdf(*objs, **kwargs): Parameters ---------- - objs : tuple of objects + *objs : object The Python objects to display, or if raw=True raw javascript data to display. raw : bool @@ -675,7 +682,7 @@ def reload(self): with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp: encoding = None data = fp.read() - + # decode data, if an encoding was specified # We only touch self.data once since # subclasses such as SVG have @data.setter methods @@ -1340,19 +1347,19 @@ def __init__(self, data=None, url=None, filename=None, embed=False, ---------- data : unicode, str or bytes The raw video data or a URL or filename to load the data from. - Raw data will require passing `embed=True`. + Raw data will require passing ``embed=True``. url : unicode - A URL for the video. If you specify `url=`, + A URL for the video. If you specify ``url=``, the image data will not be embedded. filename : unicode Path to a local file containing the video. - Will be interpreted as a local URL unless `embed=True`. + Will be interpreted as a local URL unless ``embed=True``. embed : bool Should the video be embedded using a data URI (True) or be loaded using a