From a09e5776a92805ef7b8efb3dd1caeaab4a0d8b71 2021-09-26 21:32:57 From: blois Date: 2021-09-26 21:32:57 Subject: [PATCH] Merge branch 'master' into alt_text --- diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index fbe0daa..d5be139 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -13,3 +13,9 @@ # # initial black-format # # rename something internal 6e748726282d1acb9a4f9f264ee679c474c4b8f5 # Apply pygrade --36plus on IPython/core/tests/test_inputtransformer.py. +0233e65d8086d0ec34acb8685b7a5411633f0899 # apply pyupgrade to IPython/extensions/tests/test_autoreload.py +a6a7e4dd7e51b892147895006d3a2a6c34b79ae6 # apply black to IPython/extensions/tests/test_autoreload.py +c5ca5a8f25432dfd6b9eccbbe446a8348bf37cfa # apply pyupgrade to IPython/extensions/autoreload.py +50624b84ccdece781750f5eb635a9efbf2fe30d6 # apply black to IPython/extensions/autoreload.py +b7aaa47412b96379198705955004930c57f9d74a # apply pyupgrade to IPython/extensions/autoreload.py +9c7476a88af3e567426b412f1b3c778401d8f6aa # apply black to IPython/extensions/autoreload.py diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 49ccc26..2a6d487 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -13,7 +13,4 @@ If it's a generic Python/Jupyter question, try other forums or discourse.jupyter If you are unsure, it's ok to post here, though, there are few maintainer so you might not get a fast response. -Ability of maintainers to spend time and resources on project like IPython is heavily influenced by US politics, and the current government policies have been harmful to the IPython Maintainers and Community. - -If you are on the fence on who to vote for or wether to vote, please cast your vote in for the democrat party in the US. --> diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..38b6ab9 --- /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/mypy.yml b/.github/workflows/mypy.yml index 2778805..03b58c6 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -2,9 +2,9 @@ name: Run MyPy on: push: - branches: [ master ] + branches: [ master, 7.x] pull_request: - branches: [ master ] + branches: [ master, 7.x] jobs: build: @@ -26,8 +26,9 @@ jobs: pip install mypy pyflakes flake8 - name: Lint with mypy run: | - mypy IPython/terminal/ptutils.py - mypy IPython/core/c*.py + mypy -p IPython.terminal + mypy -p IPython.core.magics - name: Lint with pyflakes run: | flake8 IPython/core/magics/script.py + flake8 IPython/core/magics/packaging.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 80497bb..ef24d62 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,9 +5,9 @@ name: Python package on: push: - branches: [ master ] + branches: [ master, 7.x ] pull_request: - branches: [ master ] + branches: [ master, 7.x ] jobs: build: @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install darker + pip install darker isort - name: Lint with darker run: | darker -r 60625f241f298b5039cb2debc365db38aa7bb522 --check --diff . || ( diff --git a/.github/workflows/test-osx.yml b/.github/workflows/test-osx.yml new file mode 100644 index 0000000..c9afbe0 --- /dev/null +++ b/.github/workflows/test-osx.yml @@ -0,0 +1,23 @@ +name: Run tests on OSX + +on: [push, pull_request] + +jobs: + test: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - 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 anyio + - name: pytest + run: pytest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b9bbae4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,36 @@ +name: Run tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 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 + 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@v1 diff --git a/.gitignore b/.gitignore index 1fc0e22..5b45fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ docs/man/*.gz docs/source/api/generated docs/source/config/options docs/source/config/shortcuts/*.csv +docs/source/savefig docs/source/interactive/magics-generated.txt docs/gh-pages jupyter_notebook/notebook/static/mathjax @@ -28,3 +29,4 @@ __pycache__ .python-version venv*/ .idea/ +.mypy_cache/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index db02c50..0000000 --- a/.travis.yml +++ /dev/null @@ -1,105 +0,0 @@ -# http://travis-ci.org/#!/ipython/ipython -language: python -os: linux - -addons: - apt: - packages: - - graphviz - -python: - - 3.8 - -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 - - pip install -e file://$PWD#egg=ipython[test] --upgrade - - pip install trio curio --upgrade --upgrade-strategy eager - - pip install 'pytest' 'matplotlib !=3.2.0' - - pip install codecov check-manifest pytest-cov --upgrade anyio pytest-trio - - -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 - - | - if [[ "$TRAVIS_PYTHON_VERSION" == "3.8" ]] && [[ "$TRAVIS_OS_NAME" == "linux" ]]; then - cd /tmp && iptest --coverage xml && cd - - fi - - pytest --maxfail=10 IPython - # On the latest Python (on Linux) only, make sure that the docs build. - - | - if [[ "$TRAVIS_PYTHON_VERSION" == "3.8" ]] && [[ "$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 - - arch: amd64 - python: "3.8" - dist: xenial - - arch: amd64 - python: "nightly" - dist: xenial - - arch: amd64 - python: "3.9-dev" - - 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 - cleanup: false - 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 4fb7710..0b182bd 100644 --- a/IPython/__init__.py +++ b/IPython/__init__.py @@ -68,20 +68,19 @@ version_info = release.version_info def embed_kernel(module=None, local_ns=None, **kwargs): """Embed and start an IPython kernel in a given scope. - + If you don't want the kernel to initialize the namespace from the scope of the surrounding function, and/or you want to load full IPython configuration, you probably want `IPython.start_kernel()` instead. - + Parameters ---------- module : types.ModuleType, optional The module to load into IPython globals (default: caller) local_ns : dict, optional The namespace to load into IPython user namespace (default: caller) - - kwargs : various, optional + **kwargs : various, optional Further keyword args are relayed to the IPKernelApp constructor, allowing configuration of the Kernel. Will only have an effect on the first embed_kernel call for a given process. @@ -99,26 +98,25 @@ def embed_kernel(module=None, local_ns=None, **kwargs): def start_ipython(argv=None, **kwargs): """Launch a normal IPython instance (as opposed to embedded) - + `IPython.embed()` puts a shell in a particular calling scope, such as a function or method for debugging purposes, which is often not desirable. - + `start_ipython()` does full, regular IPython initialization, including loading startup files, configuration, etc. much of which is skipped by `embed()`. - + This is a public API method, and will survive implementation changes. - + Parameters ---------- - argv : list or None, optional If unspecified or None, IPython will parse command-line options from sys.argv. To prevent any command-line parsing, pass an empty list: `argv=[]`. user_ns : dict, optional specify this dictionary to initialize the IPython user namespace with particular values. - kwargs : various, optional + **kwargs : various, optional Any other kwargs will be passed to the Application constructor, such as `config`. """ @@ -127,24 +125,23 @@ def start_ipython(argv=None, **kwargs): def start_kernel(argv=None, **kwargs): """Launch a normal IPython kernel instance (as opposed to embedded) - + `IPython.embed_kernel()` puts a shell in a particular calling scope, such as a function or method for debugging purposes, which is often not desirable. - + `start_kernel()` does full, regular IPython initialization, including loading startup files, configuration, etc. much of which is skipped by `embed()`. - + Parameters ---------- - argv : list or None, optional If unspecified or None, IPython will parse command-line options from sys.argv. To prevent any command-line parsing, pass an empty list: `argv=[]`. user_ns : dict, optional specify this dictionary to initialize the IPython user namespace with particular values. - kwargs : various, optional + **kwargs : various, optional Any other kwargs will be passed to the Application constructor, such as `config`. """ diff --git a/IPython/core/application.py b/IPython/core/application.py index 2aae062..db48ab2 100644 --- a/IPython/core/application.py +++ b/IPython/core/application.py @@ -66,27 +66,49 @@ else: # aliases and flags -base_aliases = { - 'profile-dir' : 'ProfileDir.location', - 'profile' : 'BaseIPythonApplication.profile', - 'ipython-dir' : 'BaseIPythonApplication.ipython_dir', - 'log-level' : 'Application.log_level', - 'config' : 'BaseIPythonApplication.extra_config_file', -} - -base_flags = dict( - debug = ({'Application' : {'log_level' : logging.DEBUG}}, - "set log level to logging.DEBUG (maximize logging output)"), - quiet = ({'Application' : {'log_level' : logging.CRITICAL}}, - "set log level to logging.CRITICAL (minimize logging output)"), - init = ({'BaseIPythonApplication' : { - 'copy_config_files' : True, - 'auto_create' : True} - }, """Initialize profile with default config files. This is equivalent +base_aliases = {} +if isinstance(Application.aliases, dict): + # traitlets 5 + base_aliases.update(Application.aliases) +base_aliases.update( + { + "profile-dir": "ProfileDir.location", + "profile": "BaseIPythonApplication.profile", + "ipython-dir": "BaseIPythonApplication.ipython_dir", + "log-level": "Application.log_level", + "config": "BaseIPythonApplication.extra_config_file", + } +) + +base_flags = dict() +if isinstance(Application.flags, dict): + # traitlets 5 + base_flags.update(Application.flags) +base_flags.update( + dict( + debug=( + {"Application": {"log_level": logging.DEBUG}}, + "set log level to logging.DEBUG (maximize logging output)", + ), + quiet=( + {"Application": {"log_level": logging.CRITICAL}}, + "set log level to logging.CRITICAL (minimize logging output)", + ), + init=( + { + "BaseIPythonApplication": { + "copy_config_files": True, + "auto_create": True, + } + }, + """Initialize profile with default config files. This is equivalent to running `ipython profile create ` prior to startup. - """) + """, + ), + ) ) + class ProfileAwareConfigLoader(PyFileConfigLoader): """A Python file config loader that is aware of IPython profiles.""" def load_subconfig(self, fname, path=None, profile=None): @@ -254,7 +276,7 @@ class BaseIPythonApplication(Application): def excepthook(self, etype, evalue, tb): """this is sys.excepthook after init_crashhandler - + set self.verbose_crash=True to use our full crashhandler, instead of a regular traceback with a short message (crash_handler_lite) """ diff --git a/IPython/core/autocall.py b/IPython/core/autocall.py index bab7f85..5f7720b 100644 --- a/IPython/core/autocall.py +++ b/IPython/core/autocall.py @@ -41,9 +41,9 @@ class IPyAutocall(object): def set_ip(self, ip): """ Will be used to set _ip point to current ipython instance b/f call - + Override this method if you don't want this to happen. - + """ self._ip = ip diff --git a/IPython/core/compilerop.py b/IPython/core/compilerop.py index 50672a1..b43e570 100644 --- a/IPython/core/compilerop.py +++ b/IPython/core/compilerop.py @@ -92,6 +92,10 @@ class CachingCompiler(codeop.Compile): # (otherwise we'd lose our tracebacks). linecache.checkcache = check_linecache_ipython + # Caching a dictionary { filename: execution_count } for nicely + # rendered tracebacks. The filename corresponds to the filename + # argument used for the builtins.compile function. + self._filename_map = {} def ast_parse(self, source, filename='', symbol='exec'): """Parse code to an AST with the current compiler flags active. @@ -118,12 +122,12 @@ class CachingCompiler(codeop.Compile): Parameters ---------- raw_code : str - The raw cell code. + The raw cell code. transformed_code : str - The executable Python source code to cache and compile. + 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. + A number which forms part of the code's name. Used for the execution + counter. Returns ------- @@ -137,12 +141,12 @@ class CachingCompiler(codeop.Compile): Parameters ---------- transformed_code : str - The executable Python source code to cache and compile. + 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. + 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`. + The raw code before transformation, if None, set to `transformed_code`. Returns ------- @@ -153,6 +157,10 @@ class CachingCompiler(codeop.Compile): raw_code = transformed_code name = self.get_code_name(raw_code, transformed_code, number) + + # Save the execution count + self._filename_map[name] = number + entry = ( len(transformed_code), time.time(), diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 78bc57e..9237929 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -431,9 +431,9 @@ def _deduplicate_completions(text: str, completions: _IC)-> _IC: Parameters ---------- - text: str + text : str text that should be completed. - completions: Iterator[Completion] + completions : Iterator[Completion] iterator over the completions to deduplicate Yields @@ -474,9 +474,9 @@ def rectify_completions(text: str, completions: _IC, *, _debug=False)->_IC: Parameters ---------- - text: str + text : str text that should be completed. - completions: Iterator[Completion] + completions : Iterator[Completion] iterator over the completions to rectify Notes @@ -763,13 +763,13 @@ def match_dict_keys(keys: List[Union[str, bytes, Tuple[Union[str, bytes]]]], pre Parameters ---------- - keys: + keys list of keys in dictionary currently being completed. - prefix: + prefix Part of the text already typed by the user. E.g. `mydict[b'fo` - delims: + delims String of delimiters to consider when finding the current key. - extra_prefix: optional + extra_prefix : optional Part of the text already typed in multi-key index cases. E.g. for `mydict['foo', "bar", 'b`, this would be `('foo', 'bar')`. @@ -993,7 +993,7 @@ def _formatparamchildren(parameter) -> str: Parameters ---------- - parameter: + parameter Jedi's function `Param` Returns @@ -1013,7 +1013,7 @@ def _make_signature(completion)-> str: Parameters ---------- - completion: jedi.Completion + completion : jedi.Completion object does not complete a function type Returns @@ -1854,9 +1854,9 @@ class IPCompleter(Completer): Parameters ---------- - text:str + text : str Full text of the current input, multi line string. - offset:int + offset : int Integer representing the position of the cursor in ``text``. Offset is 0-based indexed. diff --git a/IPython/core/completerlib.py b/IPython/core/completerlib.py index 7860cb6..0ca97e7 100644 --- a/IPython/core/completerlib.py +++ b/IPython/core/completerlib.py @@ -154,6 +154,17 @@ def is_importable(module, attr, only_modules): else: return not(attr[:2] == '__' and attr[-2:] == '__') +def is_possible_submodule(module, attr): + try: + obj = getattr(module, attr) + except AttributeError: + # Is possilby an unimported submodule + return True + except TypeError: + # https://github.com/ipython/ipython/issues/9678 + return False + return inspect.ismodule(obj) + def try_import(mod: str, only_modules=False) -> List[str]: """ @@ -172,7 +183,12 @@ def try_import(mod: str, only_modules=False) -> List[str]: completions.extend( [attr for attr in dir(m) if is_importable(m, attr, only_modules)]) - completions.extend(getattr(m, '__all__', [])) + m_all = getattr(m, "__all__", []) + if only_modules: + completions.extend(attr for attr in m_all if is_possible_submodule(m, attr)) + else: + completions.extend(m_all) + if m_is_init: completions.extend(module_list(os.path.dirname(m.__file__))) completions_set = {c for c in completions if isinstance(c, str)} diff --git a/IPython/core/crashhandler.py b/IPython/core/crashhandler.py index 2cfe85c..181fc15 100644 --- a/IPython/core/crashhandler.py +++ b/IPython/core/crashhandler.py @@ -101,25 +101,19 @@ class CrashHandler(object): Parameters ---------- - app : Application + app : Application A running :class:`Application` instance, which will be queried at crash time for internal information. - contact_name : str A string with the name of the person to contact. - contact_email : str A string with the email address of the contact. - bug_tracker : str A string with the URL for your project's bug tracker. - show_crash_traceback : bool If false, don't print the crash traceback on stderr, only generate the on-disk report - - Non-argument instance attributes: - + Non-argument instance attributes These instances contain some non-argument attributes which allow for further customization of the crash handler's behavior. Please see the source for further details. diff --git a/IPython/core/debugger.py b/IPython/core/debugger.py index b0e62ca..e2676a6 100644 --- a/IPython/core/debugger.py +++ b/IPython/core/debugger.py @@ -33,6 +33,7 @@ import linecache import sys import warnings import re +import os from IPython import get_ipython from IPython.utils import PyColorize @@ -109,7 +110,6 @@ class Tracer(object): Parameters ---------- - colors : str, optional The name of the color scheme to use, it must be one of IPython's valid color schemes. If not given, the function will default to @@ -199,21 +199,39 @@ 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} + 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,6 +299,10 @@ class Pdb(OldPdb): # 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.""" @@ -293,6 +315,26 @@ class Pdb(OldPdb): 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 + else: + return self._get_frame_locals(frame).get("__tracebackhide__", False) + + return False + def hidden_frames(self, stack): """ Given an index in the stack return whether it should be skipped. @@ -303,14 +345,9 @@ class Pdb(OldPdb): # 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] in (self.curframe, getattr(self, "initial_frame", None)) - 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 @@ -398,6 +435,28 @@ class Pdb(OldPdb): 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 @@ -422,10 +481,11 @@ class Pdb(OldPdb): 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` + ')' @@ -437,10 +497,10 @@ class Pdb(OldPdb): 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) @@ -533,15 +593,68 @@ class Pdb(OldPdb): 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 @@ -581,7 +694,7 @@ class Pdb(OldPdb): 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): @@ -705,12 +818,14 @@ class Pdb(OldPdb): def stop_here(self, frame): hidden = False if self.skip_hidden: - hidden = frame.f_locals.get("__tracebackhide__", False) + hidden = self._hidden_predicate(frame) if hidden: - Colors = self.color_scheme_table.active_colors - ColorsNormal = Colors.Normal - print(f"{Colors.excName} [... skipped 1 hidden frame]{ColorsNormal}\n") - + 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 super().stop_here(frame) def do_up(self, arg): @@ -721,7 +836,7 @@ class Pdb(OldPdb): Will skip hidden frames. """ # modified version of upstream that skips - # frames with __tracebackide__ + # frames with __tracebackhide__ if self.curindex == 0: self.error("Oldest frame") return diff --git a/IPython/core/display.py b/IPython/core/display.py index 00fe85e..310e2fa 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -1178,7 +1178,12 @@ class Video(DisplayObject): @skip_doctest def set_matplotlib_formats(*formats, **kwargs): - """Select figure formats for the inline backend. Optionally pass quality for JPEG. + """ + .. deprecated:: 7.23 + + use `matplotlib_inline.backend_inline.set_matplotlib_formats()` + + Select figure formats for the inline backend. Optionally pass quality for JPEG. For example, this enables PNG and JPEG output with a JPEG quality of 90%:: @@ -1196,20 +1201,28 @@ def set_matplotlib_formats(*formats, **kwargs): **kwargs Keyword args will be relayed to ``figure.canvas.print_figure``. """ - from IPython.core.interactiveshell import InteractiveShell - from IPython.core.pylabtools import select_figure_formats - # build kwargs, starting with InlineBackend config - kw = {} - from ipykernel.pylab.config import InlineBackend - cfg = InlineBackend.instance() - kw.update(cfg.print_figure_kwargs) - kw.update(**kwargs) - shell = InteractiveShell.instance() - select_figure_formats(shell, formats, **kw) + warnings.warn( + "`set_matplotlib_formats` is deprecated since IPython 7.23, directly " + "use `matplotlib_inline.backend_inline.set_matplotlib_formats()`", + DeprecationWarning, + stacklevel=2, + ) + + from matplotlib_inline.backend_inline import ( + set_matplotlib_formats as set_matplotlib_formats_orig, + ) + + set_matplotlib_formats_orig(*formats, **kwargs) @skip_doctest def set_matplotlib_close(close=True): - """Set whether the inline backend closes all figures automatically or not. + """ + .. deprecated:: 7.23 + + use `matplotlib_inline.backend_inline.set_matplotlib_close()` + + + Set whether the inline backend closes all figures automatically or not. By default, the inline backend used in the IPython Notebook will close all matplotlib figures automatically after each cell is run. This means that @@ -1229,6 +1242,15 @@ def set_matplotlib_close(close=True): Should all matplotlib figures be automatically closed after each cell is run? """ - from ipykernel.pylab.config import InlineBackend - cfg = InlineBackend.instance() - cfg.close_figures = close + warnings.warn( + "`set_matplotlib_close` is deprecated since IPython 7.23, directly " + "use `matplotlib_inline.backend_inline.set_matplotlib_close()`", + DeprecationWarning, + stacklevel=2, + ) + + from matplotlib_inline.backend_inline import ( + set_matplotlib_close as set_matplotlib_close_orig, + ) + + set_matplotlib_close_orig(close) diff --git a/IPython/core/display_functions.py b/IPython/core/display_functions.py index 5b49faa..aab2b7e 100644 --- a/IPython/core/display_functions.py +++ b/IPython/core/display_functions.py @@ -62,7 +62,7 @@ def publish_display_data(data, metadata=None, source=None, *, transient=None, ** Unused. transient : dict, keyword-only A dictionary of transient data, such as display_id. - """ + """ from IPython.core.interactiveshell import InteractiveShell display_pub = InteractiveShell.instance().display_pub @@ -85,7 +85,17 @@ def _new_id(): return b2a_hex(os.urandom(16)).decode('ascii') -def display(*objs, include=None, exclude=None, metadata=None, transient=None, display_id=None, **kwargs): +def display( + *objs, + include=None, + exclude=None, + metadata=None, + transient=None, + display_id=None, + raw=False, + clear=False, + **kwargs +): """Display a Python object in all frontends. By default all representations will be computed and sent to the frontends. @@ -120,12 +130,14 @@ 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` - kwargs: additional keyword-args, optional + 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. Returns ------- - handle: DisplayHandle Returns a handle on updatable displays for use with :func:`update_display`, if `display_id` is given. Returns :any:`None` if no `display_id` is given @@ -133,7 +145,6 @@ def display(*objs, include=None, exclude=None, metadata=None, transient=None, di Examples -------- - >>> class Json(object): ... def __init__(self, json): ... self.json = json @@ -172,12 +183,10 @@ def display(*objs, include=None, exclude=None, metadata=None, transient=None, di See Also -------- - :func:`update_display` Notes ----- - In Python, objects can declare their textual representation using the `__repr__` method. IPython expands on this idea and allows objects to declare other, rich representations including: @@ -239,7 +248,6 @@ def display(*objs, include=None, exclude=None, metadata=None, transient=None, di print(*objs) return - raw = kwargs.pop('raw', False) if transient is None: transient = {} if metadata is None: @@ -263,6 +271,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) @@ -285,15 +296,13 @@ def update_display(obj, *, display_id, **kwargs): Parameters ---------- - - obj: + obj The object with which to update the display - display_id: keyword-only + display_id : keyword-only The id of the display to update See Also -------- - :func:`display` """ kwargs['update'] = True @@ -328,10 +337,9 @@ class DisplayHandle(object): Parameters ---------- - - obj: + obj object to display - **kwargs: + **kwargs additional keyword arguments passed to display """ display(obj, display_id=self.display_id, **kwargs) @@ -341,10 +349,9 @@ class DisplayHandle(object): Parameters ---------- - - obj: + obj object to display - **kwargs: + **kwargs additional keyword arguments passed to update_display """ update_display(obj, display_id=self.display_id, **kwargs) diff --git a/IPython/core/displayhook.py b/IPython/core/displayhook.py index 3c06675..578e783 100644 --- a/IPython/core/displayhook.py +++ b/IPython/core/displayhook.py @@ -146,7 +146,7 @@ class DisplayHook(Configurable): MIME type representation of the object. md_dict is a :class:`dict` with the same MIME type keys of metadata associated with each output. - + """ return self.shell.display_formatter.format(result) diff --git a/IPython/core/displaypub.py b/IPython/core/displaypub.py index 14719a2..74028ec 100644 --- a/IPython/core/displaypub.py +++ b/IPython/core/displaypub.py @@ -94,11 +94,11 @@ class DisplayPublisher(Configurable): the data itself. source : str, deprecated Unused. - transient: dict, keyword-only + transient : dict, keyword-only A dictionary for transient data. Data in this dictionary should not be persisted as part of saving this output. Examples include 'display_id'. - update: bool, keyword-only, default: False + update : bool, keyword-only, default: False If True, only update existing outputs with the same display_id, rather than creating a new output. """ diff --git a/IPython/core/events.py b/IPython/core/events.py index 1af13ca..73fc181 100644 --- a/IPython/core/events.py +++ b/IPython/core/events.py @@ -28,34 +28,34 @@ class EventManager(object): """ def __init__(self, shell, available_events): """Initialise the :class:`CallbackManager`. - + Parameters ---------- shell - The :class:`~IPython.core.interactiveshell.InteractiveShell` instance - available_callbacks - An iterable of names for callback events. + The :class:`~IPython.core.interactiveshell.InteractiveShell` instance + available_events + An iterable of names for callback events. """ self.shell = shell self.callbacks = {n:[] for n in available_events} def register(self, event, function): """Register a new event callback. - + Parameters ---------- event : str - The event for which to register this callback. + The event for which to register this callback. function : callable - A function to be called on the given event. It should take the same - parameters as the appropriate callback prototype. - + A function to be called on the given event. It should take the same + parameters as the appropriate callback prototype. + Raises ------ TypeError - If ``function`` is not callable. + If ``function`` is not callable. KeyError - If ``event`` is not one of the known events. + If ``event`` is not one of the known events. """ if not callable(function): raise TypeError('Need a callable, got %r' % function) @@ -80,7 +80,7 @@ class EventManager(object): def trigger(self, event, *args, **kwargs): """Call callbacks for ``event``. - + Any additional arguments are passed to all callbacks registered for this event. Exceptions raised by callbacks are caught, and a message printed. """ @@ -109,7 +109,7 @@ def _define_event(callback_function): @_define_event def pre_execute(): """Fires before code is executed in response to user/frontend action. - + This includes comm and widget messages and silent execution, as well as user code cells. """ @@ -122,14 +122,14 @@ def pre_run_cell(info): Parameters ---------- info : :class:`~IPython.core.interactiveshell.ExecutionInfo` - An object containing information used for the code execution. + An object containing information used for the code execution. """ pass @_define_event def post_execute(): """Fires after code is executed in response to user/frontend action. - + This includes comm and widget messages and silent execution, as well as user code cells. """ @@ -142,20 +142,20 @@ def post_run_cell(result): Parameters ---------- result : :class:`~IPython.core.interactiveshell.ExecutionResult` - The object which will be returned as the execution result. + The object which will be returned as the execution result. """ pass @_define_event def shell_initialized(ip): """Fires after initialisation of :class:`~IPython.core.interactiveshell.InteractiveShell`. - + This is before extensions and startup scripts are loaded, so it can only be set by subclassing. - + Parameters ---------- ip : :class:`~IPython.core.interactiveshell.InteractiveShell` - The newly initialised shell. + The newly initialised shell. """ pass diff --git a/IPython/core/extensions.py b/IPython/core/extensions.py index e570d48..db3b040 100644 --- a/IPython/core/extensions.py +++ b/IPython/core/extensions.py @@ -94,7 +94,7 @@ class ExtensionManager(Configurable): This function looks up the extension's name in ``sys.modules`` and simply calls ``mod.unload_ipython_extension(self)``. - + Returns the string "no unload function" if the extension doesn't define a function to unload itself, "not loaded" if the extension isn't loaded, otherwise None. diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py index 4970960..07efeca 100644 --- a/IPython/core/formatters.py +++ b/IPython/core/formatters.py @@ -121,19 +121,17 @@ class DisplayFormatter(Configurable): Returns ------- (format_dict, metadata_dict) : tuple of two dicts - format_dict is a dictionary of key/value pairs, one of each format that was generated for the object. The keys are the format types, which will usually be MIME type strings and the values and JSON'able data structure containing the raw data for the representation in that format. - + metadata_dict is a dictionary of metadata about each mime-type output. Its keys will be a strict subset of the keys in format_dict. Notes ----- - If an object implement `_repr_mimebundle_` as well as various `_repr_*_`, the data returned by `_repr_mimebundle_` will take precedence and the corresponding `_repr_*_` for this mimetype will @@ -263,7 +261,7 @@ class FormatterABC(metaclass=abc.ABCMeta): def _mod_name_key(typ): """Return a (__module__, __name__) tuple for a type. - + Used as key in Formatter.deferred_printers. """ module = getattr(typ, '__module__', None) @@ -358,7 +356,7 @@ class BaseFormatter(Configurable): def _check_return(self, r, obj): """Check that a return value is appropriate - + Return the value if so, None otherwise, warning if invalid. """ if r is None or isinstance(r, self._return_type) or \ @@ -373,10 +371,10 @@ class BaseFormatter(Configurable): def lookup(self, obj): """Look up the formatter for a given instance. - + Parameters ---------- - obj : object instance + obj : object instance Returns ------- @@ -399,7 +397,7 @@ class BaseFormatter(Configurable): Parameters ---------- - typ : type or '__module__.__name__' string for a type + typ : type or '__module__.__name__' string for a type Returns ------- @@ -430,7 +428,7 @@ class BaseFormatter(Configurable): def for_type(self, typ, func=None): """Add a format function for a given type. - + Parameters ---------- typ : type or '__module__.__name__' string for a type @@ -441,10 +439,10 @@ class BaseFormatter(Configurable): and will return the raw data in this formatter's format. Subclasses may use a different call signature for the `func` argument. - + If `func` is None or not specified, there will be no change, only returning the current value. - + Returns ------- oldfunc : callable @@ -484,10 +482,10 @@ class BaseFormatter(Configurable): and will return the raw data in this formatter's format. Subclasses may use a different call signature for the `func` argument. - + If `func` is None or unspecified, there will be no change, only returning the current value. - + Returns ------- oldfunc : callable @@ -636,7 +634,6 @@ class PlainTextFormatter(BaseFormatter): This parameter can be set via the '%precision' magic. """ - new = change['new'] if '%' in new: # got explicit format string @@ -678,6 +675,11 @@ class PlainTextFormatter(BaseFormatter): def _type_printers_default(self): d = pretty._type_pprinters.copy() d[float] = lambda obj,p,cycle: p.text(self.float_format%obj) + # if NumPy is used, set precision for its float64 type + if "numpy" in sys.modules: + import numpy + + d[numpy.float64] = lambda obj, p, cycle: p.text(self.float_format % obj) return d @default('deferred_printers') @@ -823,7 +825,7 @@ class JSONFormatter(BaseFormatter): def _check_return(self, r, obj): """Check that a return value is appropriate - + Return the value if so, None otherwise, warning if invalid. """ if r is None: diff --git a/IPython/core/history.py b/IPython/core/history.py index 08db18f..ff931d2 100644 --- a/IPython/core/history.py +++ b/IPython/core/history.py @@ -445,8 +445,11 @@ class HistoryAccessor(HistoryAccessorBase): Parameters ---------- rangestr : str - A string specifying ranges, e.g. "5 ~2/1-4". See - :func:`magic_history` for full details. + A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used, + this will return everything from current session's history. + + See the documentation of :func:`%history` for the full details. + raw, output : bool As :meth:`get_range` @@ -851,11 +854,18 @@ $""", re.VERBOSE) def extract_hist_ranges(ranges_str): """Turn a string of history ranges into 3-tuples of (session, start, stop). + Empty string results in a `[(0, 1, None)]`, i.e. "everything from current + session". + Examples -------- >>> list(extract_hist_ranges("~8/5-~7/4 2")) [(-8, 5, None), (-7, 1, 5), (0, 2, 3)] """ + if ranges_str == "": + yield (0, 1, None) # Everything from current session + return + for range_str in ranges_str.split(): rmatch = range_re.match(range_str) if not rmatch: diff --git a/IPython/core/inputtransformer2.py b/IPython/core/inputtransformer2.py index 44b44de..53e114c 100644 --- a/IPython/core/inputtransformer2.py +++ b/IPython/core/inputtransformer2.py @@ -10,7 +10,9 @@ deprecated in 7.0. # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -from codeop import compile_command +import ast +import sys +from codeop import CommandCompiler, Compile import re import tokenize from typing import List, Tuple, Optional, Any @@ -89,7 +91,30 @@ classic_prompt = PromptStripper( initial_re=re.compile(r'^>>>( |$)') ) -ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')) +ipython_prompt = PromptStripper( + re.compile( + r""" + ^( # Match from the beginning of a line, either: + + # 1. First-line prompt: + ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there + In\ # The 'In' of the prompt, with a space + \[\d+\]: # Command index, as displayed in the prompt + \ # With a mandatory trailing space + + | # ... or ... + + # 2. The three dots of the multiline prompt + \s* # All leading whitespace characters + \.{3,}: # The three (or more) dots + \ ? # With an optional trailing space + + ) + """, + re.VERBOSE, + ) +) + def cell_magic(lines): if not lines or not lines[0].startswith('%%'): @@ -508,6 +533,20 @@ def make_tokens_by_line(lines:List[str]): return tokens_by_line + +def has_sunken_brackets(tokens: List[tokenize.TokenInfo]): + """Check if the depth of brackets in the list of tokens drops below 0""" + parenlev = 0 + for token in tokens: + if token.string in {"(", "[", "{"}: + parenlev += 1 + elif token.string in {")", "]", "}"}: + parenlev -= 1 + if parenlev < 0: + return True + return False + + def show_linewise_tokens(s: str): """For investigation and debugging""" if not s.endswith('\n'): @@ -662,6 +701,15 @@ class TransformerManager: tokens_by_line = make_tokens_by_line(lines) + # Bail if we got one line and there are more closing parentheses than + # the opening ones + if ( + len(lines) == 1 + and tokens_by_line + and has_sunken_brackets(tokens_by_line[0]) + ): + return "invalid", None + if not tokens_by_line: return 'incomplete', find_last_indent(lines) @@ -727,3 +775,25 @@ def find_last_indent(lines): if not m: return 0 return len(m.group(0).replace('\t', ' '*4)) + + +class MaybeAsyncCompile(Compile): + def __init__(self, extra_flags=0): + super().__init__() + self.flags |= extra_flags + + def __call__(self, *args, **kwds): + return compile(*args, **kwds) + + +class MaybeAsyncCommandCompiler(CommandCompiler): + def __init__(self, extra_flags=0): + self.compiler = MaybeAsyncCompile(extra_flags=extra_flags) + + +if (sys.version_info.major, sys.version_info.minor) >= (3, 8): + _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT +else: + _extra_flags = ast.PyCF_ONLY_AST + +compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags) diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index 589a3be..37b3158 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -921,14 +921,13 @@ class InteractiveShell(SingletonConfigurable): while p.is_symlink(): p = Path(os.readlink(p)) paths.append(p.resolve()) - + # In Cygwin paths like "c:\..." and '\cygdrive\c\...' are possible - if str(p_venv).startswith("\\cygdrive"): - p_venv = Path(str(p_venv)[11:]) - elif len(str(p_venv)) >= 2 and str(p_venv)[1] == ":": - p_venv = Path(str(p_venv)[2:]) + if p_venv.parts[1] == "cygdrive": + drive_name = p_venv.parts[2] + p_venv = (drive_name + ":/") / Path(*p_venv.parts[3:]) - if any(os.fspath(p_venv) in os.fspath(p) for p in paths): + if any(p_venv == p.parents[1] for p in paths): # Our exe is inside or has access to the virtualenv, don't need to do anything. return @@ -1896,9 +1895,14 @@ class InteractiveShell(SingletonConfigurable): exception or returns an invalid result, it will be immediately disabled. + Notes + ----- + WARNING: by putting in your own exception handler into IPython's main execution loop, you run a very good chance of nasty crashes. This - facility should only be used if you really know what you are doing.""" + facility should only be used if you really know what you are doing. + """ + if not isinstance(exc_tuple, tuple): raise TypeError("The custom exceptions must be given as a tuple.") @@ -1966,7 +1970,7 @@ class InteractiveShell(SingletonConfigurable): sys.excepthook themselves. I guess this is a feature that enables them to keep running after exceptions that would otherwise kill their mainloop. This is a bother for IPython - which excepts to catch all of the program exceptions with a try: + which expects to catch all of the program exceptions with a try: except: statement. Normally, IPython sets sys.excepthook to a CrashHandler instance, so if @@ -2085,13 +2089,17 @@ class InteractiveShell(SingletonConfigurable): except KeyboardInterrupt: print('\n' + self.get_exception_only(), file=sys.stderr) - def _showtraceback(self, etype, evalue, stb): + def _showtraceback(self, etype, evalue, stb: str): """Actually show a traceback. Subclasses may override this method to put the traceback on a different place, like a side channel. """ - print(self.InteractiveTB.stb2text(stb)) + val = self.InteractiveTB.stb2text(stb) + try: + print(val) + except UnicodeEncodeError: + print(val.encode("utf-8", "backslashreplace").decode()) def showsyntaxerror(self, filename=None, running_compiled_code=False): """Display the syntax error that just occurred. @@ -2212,12 +2220,15 @@ class InteractiveShell(SingletonConfigurable): Returns ------- - text : string - The actual text that was completed. + text : string + The actual text that was completed. + + matches : list + A sorted list with all possible completions. - matches : list - A sorted list with all possible completions. + Notes + ----- The optional arguments allow the completion to take more context into account, and are part of the low-level completion API. @@ -2226,7 +2237,8 @@ class InteractiveShell(SingletonConfigurable): exposing it as a method, it can be used by other non-readline environments (such as GUIs) for text completion. - Simple usage example: + Examples + -------- In [1]: x = 'hello' @@ -2508,6 +2520,23 @@ class InteractiveShell(SingletonConfigurable): Command to execute. """ cmd = self.var_expand(cmd, depth=1) + # warn if there is an IPython magic alternative. + main_cmd = cmd.split()[0] + has_magic_alternatives = ("pip", "conda", "cd", "ls") + + # had to check if the command was an alias expanded because of `ls` + is_alias_expanded = self.alias_manager.is_alias(main_cmd) and ( + self.alias_manager.retrieve_alias(main_cmd).strip() == cmd.strip() + ) + + if main_cmd in has_magic_alternatives and not is_alias_expanded: + warnings.warn( + ( + "You executed the system command !{0} which may not work " + "as expected. Try the IPython magic %{0} instead." + ).format(main_cmd) + ) + # protect os.system from UNC paths on Windows, which it can't handle: if sys.platform == 'win32': from IPython.utils._process_win32 import AvoidUNCPath @@ -3109,9 +3138,7 @@ class InteractiveShell(SingletonConfigurable): _run_async = False with self.builtin_trap: - cell_name = self.compile.cache( - cell, self.execution_count, raw_code=raw_cell - ) + cell_name = compiler.cache(cell, self.execution_count, raw_code=raw_cell) with self.display_trap: # Compile to bytecode @@ -3514,6 +3541,7 @@ class InteractiveShell(SingletonConfigurable): display figures inline. """ from IPython.core import pylabtools as pt + from matplotlib_inline.backend_inline import configure_inline_support gui, backend = pt.find_gui_and_backend(gui, self.pylab_gui_select) if gui != 'inline': @@ -3527,7 +3555,7 @@ class InteractiveShell(SingletonConfigurable): gui, backend = pt.find_gui_and_backend(self.pylab_gui_select) pt.activate_matplotlib(backend) - pt.configure_inline_support(self, backend) + configure_inline_support(self, backend) # Now we must activate the gui pylab wants to use, and fix %run to take # plot updates into account @@ -3667,12 +3695,15 @@ class InteractiveShell(SingletonConfigurable): Parameters ---------- - range_str : string + range_str : str The set of slices is given as a string, like "~5/6-~4/2 4:8 9", since this function is for use by magic functions which get their arguments as strings. The number before the / is the session number: ~n goes n back from the current session. + If empty string is given, returns history of current session + without the last input. + raw : bool, optional By default, the processed input is used. If this is true, the raw input history is used instead. @@ -3686,7 +3717,16 @@ class InteractiveShell(SingletonConfigurable): * ``N-M`` -> include items N..M (closed endpoint). """ lines = self.history_manager.get_range_by_str(range_str, raw=raw) - return "\n".join(x for _, _, x in lines) + text = "\n".join(x for _, _, x in lines) + + # Skip the last line, as it's probably the magic that called this + if not range_str: + if "\n" not in text: + text = "" + else: + text = text[: text.rfind("\n")] + + return text def find_user_code(self, target, raw=True, py_only=False, skip_encoding_cookie=True, search_ns=False): """Get a code string from history, file, url, or a string or macro. @@ -3695,14 +3735,15 @@ class InteractiveShell(SingletonConfigurable): Parameters ---------- - target : str - A string specifying code to retrieve. This will be tried respectively as: ranges of input history (see %history for syntax), url, corresponding .py file, filename, or an expression evaluating to a string or Macro in the user namespace. + If empty string is given, returns complete history of current + session, without the last line. + raw : bool If true (default), retrieve raw history. Has no effect on the other retrieval mechanisms. @@ -3771,6 +3812,22 @@ class InteractiveShell(SingletonConfigurable): raise TypeError("%s is neither a string nor a macro." % target, codeobj) + def _atexit_once(self): + """ + At exist operation that need to be called at most once. + Second call to this function per instance will do nothing. + """ + + if not getattr(self, "_atexit_once_called", False): + self._atexit_once_called = True + # Clear all user namespaces to release all references cleanly. + self.reset(new_session=False) + # Close the history session (this stores the end time and line count) + # this must be *before* the tempfile cleanup, in case of temporary + # history db + self.history_manager.end_session() + self.history_manager = None + #------------------------------------------------------------------------- # Things related to IPython exiting #------------------------------------------------------------------------- @@ -3785,26 +3842,24 @@ class InteractiveShell(SingletonConfigurable): code that has the appropriate information, rather than trying to clutter """ - # Close the history session (this stores the end time and line count) - # this must be *before* the tempfile cleanup, in case of temporary - # history db - self.history_manager.end_session() + self._atexit_once() # Cleanup all tempfiles and folders left around for tfile in self.tempfiles: try: tfile.unlink() + self.tempfiles.remove(tfile) except FileNotFoundError: pass - + del self.tempfiles for tdir in self.tempdirs: try: tdir.rmdir() + self.tempdirs.remove(tdir) except FileNotFoundError: pass + del self.tempdirs - # Clear all user namespaces to release all references cleanly. - self.reset(new_session=False) # Run user hooks self.hooks.shutdown_hook() diff --git a/IPython/core/magic.py b/IPython/core/magic.py index e64d3aa..6dc0ad3 100644 --- a/IPython/core/magic.py +++ b/IPython/core/magic.py @@ -618,6 +618,9 @@ class Magics(Configurable): posix = kw.get('posix', os.name == 'posix') strict = kw.get('strict', True) + preserve_non_opts = kw.get("preserve_non_opts", False) + remainder_arg_str = arg_str + # Check if we have more than one argument to warrant extra processing: odict = {} # Dictionary with options args = arg_str.split() @@ -629,10 +632,18 @@ class Magics(Configurable): try: opts,args = getopt(argv, opt_str, long_opts) except GetoptError as e: - raise UsageError('%s ( allowed: "%s" %s)' % (e.msg,opt_str, - " ".join(long_opts))) from e - for o,a in opts: - if o.startswith('--'): + raise UsageError( + '%s ( allowed: "%s" %s)' % (e.msg, opt_str, " ".join(long_opts)) + ) from e + for o, a in opts: + if mode == "string" and preserve_non_opts: + # remove option-parts from the original args-string and preserve remaining-part. + # This relies on the arg_split(...) and getopt(...)'s impl spec, that the parsed options are + # returned in the original order. + remainder_arg_str = remainder_arg_str.replace(o, "", 1).replace( + a, "", 1 + ) + if o.startswith("--"): o = o[2:] else: o = o[1:] @@ -649,7 +660,10 @@ class Magics(Configurable): # Prepare opts,args for return opts = Struct(odict) if mode == 'string': - args = ' '.join(args) + if preserve_non_opts: + args = remainder_arg_str.lstrip() + else: + args = " ".join(args) return opts,args diff --git a/IPython/core/magics/auto.py b/IPython/core/magics/auto.py index a18542f..56aa4f7 100644 --- a/IPython/core/magics/auto.py +++ b/IPython/core/magics/auto.py @@ -104,16 +104,32 @@ class AutoMagics(Magics): # all-random (note for auto-testing) """ + valid_modes = { + 0: "Off", + 1: "Smart", + 2: "Full", + } + + def errorMessage() -> str: + error = "Valid modes: " + for k, v in valid_modes.items(): + error += str(k) + "->" + v + ", " + error = error[:-2] # remove tailing `, ` after last element + return error + if parameter_s: + if not parameter_s in map(str, valid_modes.keys()): + error(errorMessage()) + return arg = int(parameter_s) else: arg = 'toggle' - if not arg in (0, 1, 2, 'toggle'): - error('Valid modes: (0->Off, 1->Smart, 2->Full') + if not arg in (*list(valid_modes.keys()), "toggle"): + error(errorMessage()) return - if arg in (0, 1, 2): + if arg in (valid_modes.keys()): self.shell.autocall = arg else: # toggle if self.shell.autocall: @@ -125,4 +141,4 @@ class AutoMagics(Magics): except AttributeError: self.shell.autocall = self._magic_state.autocall_save = 1 - print("Automatic calling is:",['OFF','Smart','Full'][self.shell.autocall]) + print("Automatic calling is:", list(valid_modes.values())[self.shell.autocall]) diff --git a/IPython/core/magics/basic.py b/IPython/core/magics/basic.py index a8feb75..72cfc80 100644 --- a/IPython/core/magics/basic.py +++ b/IPython/core/magics/basic.py @@ -493,6 +493,7 @@ Currently the magic system has the following functions:""", %gui qt5 # enable PyQt5 event loop integration %gui gtk # enable PyGTK event loop integration %gui gtk3 # enable Gtk3 event loop integration + %gui gtk4 # enable Gtk4 event loop integration %gui tk # enable Tk event loop integration %gui osx # enable Cocoa event loop integration # (requires %matplotlib 1.1) diff --git a/IPython/core/magics/code.py b/IPython/core/magics/code.py index 8e231a5..e4b85e3 100644 --- a/IPython/core/magics/code.py +++ b/IPython/core/magics/code.py @@ -20,7 +20,7 @@ import re import sys import ast from itertools import chain -from urllib.request import urlopen +from urllib.request import Request, urlopen from urllib.parse import urlencode from pathlib import Path @@ -29,6 +29,7 @@ from IPython.core.error import TryNext, StdinNotImplementedError, UsageError from IPython.core.macro import Macro from IPython.core.magic import Magics, magics_class, line_magic from IPython.core.oinspect import find_file, find_source_lines +from IPython.core.release import version from IPython.testing.skipdoctest import skip_doctest from IPython.utils.contexts import preserve_keys from IPython.utils.path import get_py_filename @@ -201,6 +202,9 @@ class CodeMagics(Magics): This function uses the same syntax as %history for input ranges, then saves the lines to the filename you specify. + If no ranges are specified, saves history of the current session up to + this point. + It adds a '.py' extension to the file if you don't do so yourself, and it asks for confirmation before overwriting existing files. @@ -245,20 +249,25 @@ class CodeMagics(Magics): @line_magic def pastebin(self, parameter_s=''): - """Upload code to dpaste's paste bin, returning the URL. + """Upload code to dpaste.com, returning the URL. Usage:\\ - %pastebin [-d "Custom description"] 1-7 + %pastebin [-d "Custom description"][-e 24] 1-7 The argument can be an input history range, a filename, or the name of a string or macro. + If no arguments are given, uploads the history of this session up to + this point. + Options: - -d: Pass a custom description for the gist. The default will say + -d: Pass a custom description. The default will say "Pasted from IPython". + -e: Pass number of days for the link to be expired. + The default will be 7 days. """ - opts, args = self.parse_options(parameter_s, 'd:') + opts, args = self.parse_options(parameter_s, "d:e:") try: code = self.shell.find_user_code(args) @@ -266,13 +275,30 @@ class CodeMagics(Magics): print(e.args[0]) return - post_data = urlencode({ - "title": opts.get('d', "Pasted from IPython"), - "syntax": "python3", - "content": code - }).encode('utf-8') + expiry_days = 7 + try: + expiry_days = int(opts.get("e", 7)) + except ValueError as e: + print(e.args[0].capitalize()) + return + if expiry_days < 1 or expiry_days > 365: + print("Expiry days should be in range of 1 to 365") + return - response = urlopen("http://dpaste.com/api/v2/", post_data) + post_data = urlencode( + { + "title": opts.get("d", "Pasted from IPython"), + "syntax": "python", + "content": code, + "expiry_days": expiry_days, + } + ).encode("utf-8") + + request = Request( + "https://dpaste.com/api/v2/", + headers={"User-Agent": "IPython v{}".format(version)}, + ) + response = urlopen(request, post_data) return response.headers.get('Location') @line_magic @@ -295,6 +321,9 @@ class CodeMagics(Magics): where source can be a filename, URL, input history range, macro, or element in the user namespace + If no arguments are given, loads the history of this session up to this + point. + Options: -r : Specify lines or ranges of lines to load from the source. @@ -313,6 +342,7 @@ class CodeMagics(Magics): confirmation before loading source with more than 200 000 characters, unless -y flag is passed or if the frontend does not support raw_input:: + %load %load myscript.py %load 7-27 %load myMacro @@ -324,13 +354,7 @@ class CodeMagics(Magics): %load -n my_module.wonder_function """ opts,args = self.parse_options(arg_s,'yns:r:') - - if not args: - raise UsageError('Missing filename, URL, input history range, ' - 'macro, or element in the user namespace.') - search_ns = 'n' in opts - contents = self.shell.find_user_code(args, search_ns=search_ns) if 's' in opts: diff --git a/IPython/core/magics/execution.py b/IPython/core/magics/execution.py index e2d550e..a5cfd5f 100644 --- a/IPython/core/magics/execution.py +++ b/IPython/core/magics/execution.py @@ -1073,8 +1073,9 @@ class ExecutionMagics(Magics): does not matter as long as results from timeit.py are not mixed with those from %timeit.""" - opts, stmt = self.parse_options(line,'n:r:tcp:qo', - posix=False, strict=False) + opts, stmt = self.parse_options( + line, "n:r:tcp:qo", posix=False, strict=False, preserve_non_opts=True + ) if stmt == "" and cell is None: return @@ -1234,22 +1235,25 @@ class ExecutionMagics(Magics): CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s Wall time: 0.00 - Note that the time needed by Python to compile the given expression - will be reported if it is more than 0.1s. In this example, the - actual exponentiation is done by Python at compilation time, so while - the expression can take a noticeable amount of time to compute, that - time is purely due to the compilation: - In [5]: %time 3**9999; - CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s - Wall time: 0.00 s + .. note:: + The time needed by Python to compile the given expression will be + reported if it is more than 0.1s. - In [6]: %time 3**999999; - CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s - Wall time: 0.00 s - Compiler : 0.78 s - """ + In the example below, the actual exponentiation is done by Python + at compilation time, so while the expression can take a noticeable + amount of time to compute, that time is purely due to the + compilation:: + In [5]: %time 3**9999; + CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s + Wall time: 0.00 s + + In [6]: %time 3**999999; + CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s + Wall time: 0.00 s + Compiler : 0.78 s + """ # fail immediately if the given expression can't be compiled if line and cell: diff --git a/IPython/core/magics/history.py b/IPython/core/magics/history.py index 3b95ee7..28f91fa 100644 --- a/IPython/core/magics/history.py +++ b/IPython/core/magics/history.py @@ -184,14 +184,12 @@ class HistoryMagics(Magics): n = 10 if limit is None else limit hist = history_manager.get_tail(n, raw=raw, output=get_output) else: - if args.range: # Get history by ranges - if args.pattern: - range_pattern = "*" + " ".join(args.pattern) + "*" - print_nums = True - hist = history_manager.get_range_by_str(" ".join(args.range), - raw, get_output) - else: # Just get history for the current session - hist = history_manager.get_range(raw=raw, output=get_output) + if args.pattern: + range_pattern = "*" + " ".join(args.pattern) + "*" + print_nums = True + hist = history_manager.get_range_by_str( + " ".join(args.range), raw, get_output + ) # We could be displaying the entire history, so let's not try to pull # it into a list in memory. Anything that needs more space will just @@ -282,6 +280,7 @@ class HistoryMagics(Magics): return else: self.shell.set_next_input(cmd.rstrip()) + return print("Couldn't evaluate or find in history:", arg) @line_magic @@ -301,6 +300,14 @@ class HistoryMagics(Magics): opts, args = self.parse_options(parameter_s, 'l:g:', mode='string') if "l" in opts: # Last n lines n = int(opts['l']) + + if n == 0: + print("Requested 0 last lines - nothing to run") + return + elif n < 0: + print("Number of lines to rerun cannot be negative") + return + hist = self.shell.history_manager.get_tail(n) elif "g" in opts: # Search p = "*"+opts['g']+"*" diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py index a6e3d64..c0cb209 100644 --- a/IPython/core/magics/osm.py +++ b/IPython/core/magics/osm.py @@ -302,33 +302,36 @@ class OSMagics(Magics): """Change the current working directory. This command automatically maintains an internal list of directories - you visit during your IPython session, in the variable _dh. The - command %dhist shows this history nicely formatted. You can also - do 'cd -' to see directory history conveniently. - + you visit during your IPython session, in the variable ``_dh``. The + command :magic:`%dhist` shows this history nicely formatted. You can + also do ``cd -`` to see directory history conveniently. Usage: - cd 'dir': changes to directory 'dir'. - - cd -: changes to the last visited directory. + - ``cd 'dir'``: changes to directory 'dir'. + - ``cd -``: changes to the last visited directory. + - ``cd -``: changes to the n-th directory in the directory history. + - ``cd --foo``: change to directory that matches 'foo' in history + - ``cd -b ``: jump to a bookmark set by %bookmark + - Hitting a tab key after ``cd -b`` allows you to tab-complete + bookmark names. - cd -: changes to the n-th directory in the directory history. + .. note:: + ``cd `` is enough if there is no directory + ````, but a bookmark with the name exists. - cd --foo: change to directory that matches 'foo' in history - - cd -b : jump to a bookmark set by %bookmark - (note: cd is enough if there is no - directory , but a bookmark with the name exists.) - 'cd -b ' allows you to tab-complete bookmark names. Options: - -q: quiet. Do not print the working directory after the cd command is - executed. By default IPython's cd command does print this directory, - since the default prompts do not display path information. + -q Be quiet. Do not print the working directory after the + cd command is executed. By default IPython's cd + command does print this directory, since the default + prompts do not display path information. + + .. note:: + Note that ``!cd`` doesn't work for this purpose because the shell + where ``!command`` runs is immediately discarded after executing + 'command'. - Note that !cd doesn't work for this purpose because the shell where - !command runs is immediately discarded after executing 'command'. Examples -------- @@ -436,11 +439,11 @@ class OSMagics(Magics): Usage:\\ - %env: lists all environment variables/values - %env var: get value for var - %env var val: set value for var - %env var=val: set value for var - %env var=$val: set value for var, using python expansion if possible + :``%env``: lists all environment variables/values + :``%env var``: get value for var + :``%env var val``: set value for var + :``%env var=val``: set value for var + :``%env var=$val``: set value for var, using python expansion if possible """ if parameter_s.strip(): split = '=' if '=' in parameter_s else ' ' @@ -803,18 +806,17 @@ class OSMagics(Magics): to be Python source and will show it with syntax highlighting. This magic command can either take a local filename, an url, - an history range (see %history) or a macro as argument :: + an history range (see %history) or a macro as argument. + + If no parameter is given, prints out history of current session up to + this point. :: %pycat myscript.py %pycat 7-27 %pycat myMacro %pycat http://www.example.com/myscript.py """ - if not parameter_s: - raise UsageError('Missing filename, URL, input history range, ' - 'or macro.') - - try : + try: cont = self.shell.find_user_code(parameter_s, skip_encoding_cookie=False) except (ValueError, IOError): print("Error: no such file, variable, URL, history range or macro") diff --git a/IPython/core/magics/packaging.py b/IPython/core/magics/packaging.py index e5e6877..60fe1ac 100644 --- a/IPython/core/magics/packaging.py +++ b/IPython/core/magics/packaging.py @@ -66,7 +66,14 @@ class PackagingMagics(Magics): Usage: %pip install [pkgs] """ - self.shell.system(' '.join([sys.executable, '-m', 'pip', line])) + python = sys.executable + if sys.platform == "win32": + python = '"' + python + '"' + else: + python = shlex.quote(python) + + self.shell.system(" ".join([python, "-m", "pip", line])) + print("Note: you may need to restart the kernel to use updated packages.") @line_magic diff --git a/IPython/core/magics/pylab.py b/IPython/core/magics/pylab.py index 9a46442..0f3fff6 100644 --- a/IPython/core/magics/pylab.py +++ b/IPython/core/magics/pylab.py @@ -88,7 +88,7 @@ class PylabMagics(Magics): You can list the available backends using the -l/--list option:: In [4]: %matplotlib --list - Available matplotlib backends: ['osx', 'qt4', 'qt5', 'gtk3', 'notebook', 'wx', 'qt', 'nbagg', + Available matplotlib backends: ['osx', 'qt4', 'qt5', 'gtk3', 'gtk4', 'notebook', 'wx', 'qt', 'nbagg', 'gtk', 'tk', 'inline'] """ args = magic_arguments.parse_argstring(self.matplotlib, line) diff --git a/IPython/core/magics/script.py b/IPython/core/magics/script.py index a885e3f..d4e7b08 100644 --- a/IPython/core/magics/script.py +++ b/IPython/core/magics/script.py @@ -199,6 +199,7 @@ class ScriptMagics(Magics): _handle_stream(process.stderr, args.err, sys.stderr) ) await asyncio.wait([stdout_task, stderr_task]) + await process.wait() if sys.platform.startswith("win"): asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) @@ -212,7 +213,7 @@ class ScriptMagics(Magics): *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - stdin=asyncio.subprocess.PIPE + stdin=asyncio.subprocess.PIPE, ) ) except OSError as e: @@ -264,7 +265,11 @@ class ScriptMagics(Magics): print("Error while terminating subprocess (pid=%i): %s" % (p.pid, e)) return if args.raise_error and p.returncode!=0: - raise CalledProcessError(p.returncode, cell) + # If we get here and p.returncode is still None, we must have + # killed it but not yet seen its return code. We don't wait for it, + # in case it's stuck in uninterruptible sleep. -9 = SIGKILL + rc = p.returncode or -9 + raise CalledProcessError(rc, cell) def _run_script(self, p, cell, to_close): """callback for running the script in the background""" diff --git a/IPython/core/pylabtools.py b/IPython/core/pylabtools.py index 3af899e..35f7924 100644 --- a/IPython/core/pylabtools.py +++ b/IPython/core/pylabtools.py @@ -5,30 +5,34 @@ # Distributed under the terms of the Modified BSD License. from io import BytesIO +import warnings from IPython.core.display import _pngxy from IPython.utils.decorators import flag_calls # If user specifies a GUI, that dictates the backend, otherwise we read the # user's mpl default from the mpl rc structure -backends = {'tk': 'TkAgg', - 'gtk': 'GTKAgg', - 'gtk3': 'GTK3Agg', - 'wx': 'WXAgg', - 'qt4': 'Qt4Agg', - 'qt5': 'Qt5Agg', - 'qt': 'Qt5Agg', - 'osx': 'MacOSX', - 'nbagg': 'nbAgg', - 'notebook': 'nbAgg', - 'agg': 'agg', - 'svg': 'svg', - 'pdf': 'pdf', - 'ps': 'ps', - 'inline': 'module://ipykernel.pylab.backend_inline', - 'ipympl': 'module://ipympl.backend_nbagg', - 'widget': 'module://ipympl.backend_nbagg', - } +backends = { + "tk": "TkAgg", + "gtk": "GTKAgg", + "gtk3": "GTK3Agg", + "gtk4": "GTK4Agg", + "wx": "WXAgg", + "qt4": "Qt4Agg", + "qt5": "Qt5Agg", + "qt6": "QtAgg", + "qt": "Qt5Agg", + "osx": "MacOSX", + "nbagg": "nbAgg", + "notebook": "nbAgg", + "agg": "agg", + "svg": "svg", + "pdf": "pdf", + "ps": "ps", + "inline": "module://matplotlib_inline.backend_inline", + "ipympl": "module://ipympl.backend_nbagg", + "widget": "module://ipympl.backend_nbagg", +} # We also need a reverse backends2guis mapping that will properly choose which # GUI support to activate based on the desired matplotlib backend. For the @@ -39,17 +43,18 @@ backend2gui = dict(zip(backends.values(), backends.keys())) backend2gui['Qt4Agg'] = 'qt' # In the reverse mapping, there are a few extra valid matplotlib backends that # map to the same GUI support -backend2gui['GTK'] = backend2gui['GTKCairo'] = 'gtk' -backend2gui['GTK3Cairo'] = 'gtk3' -backend2gui['WX'] = 'wx' -backend2gui['CocoaAgg'] = 'osx' +backend2gui["GTK"] = backend2gui["GTKCairo"] = "gtk" +backend2gui["GTK3Cairo"] = "gtk3" +backend2gui["GTK4Cairo"] = "gtk4" +backend2gui["WX"] = "wx" +backend2gui["CocoaAgg"] = "osx" # And some backends that don't need GUI integration -del backend2gui['nbAgg'] -del backend2gui['agg'] -del backend2gui['svg'] -del backend2gui['pdf'] -del backend2gui['ps'] -del backend2gui['module://ipykernel.pylab.backend_inline'] +del backend2gui["nbAgg"] +del backend2gui["agg"] +del backend2gui["svg"] +del backend2gui["pdf"] +del backend2gui["ps"] +del backend2gui["module://matplotlib_inline.backend_inline"] #----------------------------------------------------------------------------- # Matplotlib utilities @@ -96,10 +101,10 @@ def figsize(sizex, sizey): def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs): """Print a figure to an image, and return the resulting file data - + Returned data will be bytes unless ``fmt='svg'``, in which case it will be unicode. - + Any keyword args are passed to fig.canvas.print_figure, such as ``quality`` or ``bbox_inches``. """ @@ -112,7 +117,7 @@ def print_figure(fig, fmt='png', bbox_inches='tight', **kwargs): if fmt == 'retina': dpi = dpi * 2 fmt = 'png' - + # build keyword args kw = { "format":fmt, @@ -162,7 +167,7 @@ def mpl_runner(safe_execfile): A function suitable for use as the ``runner`` argument of the %run magic function. """ - + def mpl_execfile(fname,*where,**kw): """matplotlib-aware wrapper around safe_execfile. @@ -243,7 +248,7 @@ def select_figure_formats(shell, formats, **kwargs): bs = "%s" % ','.join([repr(f) for f in bad]) gs = "%s" % ','.join([repr(f) for f in supported]) raise ValueError("supported formats are: %s not %s" % (gs, bs)) - + if 'png' in formats: png_formatter.for_type(Figure, lambda fig: print_figure(fig, 'png', **kwargs)) if 'retina' in formats or 'png2x' in formats: @@ -274,7 +279,7 @@ def find_gui_and_backend(gui=None, gui_select=None): Returns ------- A tuple of (gui, backend) where backend is one of ('TkAgg','GTKAgg', - 'WXAgg','Qt4Agg','module://ipykernel.pylab.backend_inline','agg'). + 'WXAgg','Qt4Agg','module://matplotlib_inline.backend_inline','agg'). """ import matplotlib @@ -308,7 +313,7 @@ def activate_matplotlib(backend): import matplotlib matplotlib.interactive(True) - + # Matplotlib had a bug where even switch_backend could not force # the rcParam to update. This needs to be set *before* the module # magic of switch_backend(). @@ -329,11 +334,11 @@ def activate_matplotlib(backend): def import_pylab(user_ns, import_all=True): """Populate the namespace with pylab-related values. - + Imports matplotlib, pylab, numpy, and everything from pylab and numpy. - + Also imports a few names from IPython (figsize, display, getfigs) - + """ # Import numpy as np/pyplot as plt are conventions we're trying to @@ -346,12 +351,12 @@ def import_pylab(user_ns, import_all=True): "plt = pyplot\n" ) exec(s, user_ns) - + if import_all: s = ("from matplotlib.pylab import *\n" "from numpy import *\n") exec(s, user_ns) - + # IPython symbols to add user_ns['figsize'] = figsize from IPython.display import display @@ -361,7 +366,12 @@ def import_pylab(user_ns, import_all=True): def configure_inline_support(shell, backend): - """Configure an IPython shell object for matplotlib use. + """ + .. deprecated: 7.23 + + use `matplotlib_inline.backend_inline.configure_inline_support()` + + Configure an IPython shell object for matplotlib use. Parameters ---------- @@ -369,51 +379,15 @@ def configure_inline_support(shell, backend): backend : matplotlib backend """ - # If using our svg payload backend, register the post-execution - # function that will pick up the results for display. This can only be - # done with access to the real shell object. - - # Note: if we can't load the inline backend, then there's no point - # continuing (such as in terminal-only shells in environments without - # zeromq available). - try: - from ipykernel.pylab.backend_inline import InlineBackend - except ImportError: - return - import matplotlib - - cfg = InlineBackend.instance(parent=shell) - cfg.shell = shell - if cfg not in shell.configurables: - shell.configurables.append(cfg) - - if backend == backends['inline']: - from ipykernel.pylab.backend_inline import flush_figures - shell.events.register('post_execute', flush_figures) - - # Save rcParams that will be overwrittern - shell._saved_rcParams = {} - for k in cfg.rc: - shell._saved_rcParams[k] = matplotlib.rcParams[k] - # load inline_rc - matplotlib.rcParams.update(cfg.rc) - new_backend_name = "inline" - else: - from ipykernel.pylab.backend_inline import flush_figures - try: - shell.events.unregister('post_execute', flush_figures) - except ValueError: - pass - if hasattr(shell, '_saved_rcParams'): - matplotlib.rcParams.update(shell._saved_rcParams) - del shell._saved_rcParams - new_backend_name = "other" - - # only enable the formats once -> don't change the enabled formats (which the user may - # has changed) when getting another "%matplotlib inline" call. - # See https://github.com/ipython/ipykernel/issues/29 - cur_backend = getattr(configure_inline_support, "current_backend", "unset") - if new_backend_name != cur_backend: - # Setup the default figure format - select_figure_formats(shell, cfg.figure_formats, **cfg.print_figure_kwargs) - configure_inline_support.current_backend = new_backend_name + warnings.warn( + "`configure_inline_support` is deprecated since IPython 7.23, directly " + "use `matplotlib_inline.backend_inline.configure_inline_support()`", + DeprecationWarning, + stacklevel=2, + ) + + from matplotlib_inline.backend_inline import ( + configure_inline_support as configure_inline_support_orig, + ) + + configure_inline_support_orig(shell, backend) diff --git a/IPython/core/tests/test_alias.py b/IPython/core/tests/test_alias.py index d990796..90971de 100644 --- a/IPython/core/tests/test_alias.py +++ b/IPython/core/tests/test_alias.py @@ -1,6 +1,6 @@ from IPython.utils.capture import capture_output -import nose.tools as nt +import pytest def test_alias_lifecycle(): name = 'test_alias1' @@ -9,8 +9,8 @@ def test_alias_lifecycle(): am.clear_aliases() am.define_alias(name, cmd) assert am.is_alias(name) - nt.assert_equal(am.retrieve_alias(name), cmd) - nt.assert_in((name, cmd), am.aliases) + assert am.retrieve_alias(name) == cmd + assert (name, cmd) in am.aliases # Test running the alias orig_system = _ip.system @@ -19,16 +19,16 @@ def test_alias_lifecycle(): try: _ip.run_cell('%{}'.format(name)) result = [c.strip() for c in result] - nt.assert_equal(result, [cmd]) + assert result == [cmd] finally: _ip.system = orig_system # Test removing the alias am.undefine_alias(name) assert not am.is_alias(name) - with nt.assert_raises(ValueError): + with pytest.raises(ValueError): am.retrieve_alias(name) - nt.assert_not_in((name, cmd), am.aliases) + assert (name, cmd) not in am.aliases def test_alias_args_error(): @@ -38,7 +38,8 @@ def test_alias_args_error(): with capture_output() as cap: _ip.run_cell('parts 1') - nt.assert_equal(cap.stderr.split(':')[0], 'UsageError') + assert cap.stderr.split(":")[0] == "UsageError" + def test_alias_args_commented(): """Check that alias correctly ignores 'commented out' args""" @@ -62,4 +63,4 @@ def test_alias_args_commented_nargs(): assert am.is_alias(alias_name) thealias = am.get_alias(alias_name) - nt.assert_equal(thealias.nargs, 1) + assert thealias.nargs == 1 diff --git a/IPython/core/tests/test_compilerop.py b/IPython/core/tests/test_compilerop.py index 4b2f715..aa531b8 100644 --- a/IPython/core/tests/test_compilerop.py +++ b/IPython/core/tests/test_compilerop.py @@ -18,7 +18,7 @@ import linecache import sys # Third-party imports -import nose.tools as nt +import pytest # Our own imports from IPython.core import compilerop @@ -30,13 +30,13 @@ from IPython.core import compilerop def test_code_name(): code = 'x=1' name = compilerop.code_name(code) - nt.assert_true(name.startswith(' ncache) + assert len(linecache.cache) > ncache def test_proper_default_encoding(): # Check we're in a proper Python 2 environment (some imports, such # as GTK, can change the default encoding, which can hide bugs.) - nt.assert_equal(sys.getdefaultencoding(), "utf-8") + assert sys.getdefaultencoding() == "utf-8" def test_cache_unicode(): cp = compilerop.CachingCompiler() ncache = len(linecache.cache) cp.cache(u"t = 'žćčšđ'") - nt.assert_true(len(linecache.cache) > ncache) + assert len(linecache.cache) > ncache def test_compiler_check_cache(): """Test the compiler properly manages the cache. diff --git a/IPython/core/tests/test_completerlib.py b/IPython/core/tests/test_completerlib.py index d112704..f87e784 100644 --- a/IPython/core/tests/test_completerlib.py +++ b/IPython/core/tests/test_completerlib.py @@ -157,6 +157,11 @@ def test_bad_module_all(): nt.assert_in('puppies', results) for r in results: nt.assert_is_instance(r, str) + + # bad_all doesn't contain submodules, but this completion + # should finish without raising an exception: + results = module_completion("import bad_all.") + nt.assert_equal(results, []) finally: sys.path.remove(testsdir) @@ -176,3 +181,14 @@ def test_module_without_init(): assert s == [] finally: sys.path.remove(tmpdir) + + +def test_valid_exported_submodules(): + """ + Test checking exported (__all__) objects are submodules + """ + results = module_completion("import os.pa") + # ensure we get a valid submodule: + nt.assert_in("os.path", results) + # ensure we don't get objects that aren't submodules: + nt.assert_not_in("os.pathconf", results) diff --git a/IPython/core/tests/test_debugger.py b/IPython/core/tests/test_debugger.py index 2525e65..35e77e4 100644 --- a/IPython/core/tests/test_debugger.py +++ b/IPython/core/tests/test_debugger.py @@ -277,14 +277,14 @@ def test_xmode_skip(): block = dedent( """ -def f(): - __tracebackhide__ = True - g() + def f(): + __tracebackhide__ = True + g() -def g(): - raise ValueError + def g(): + raise ValueError -f() + f() """ ) @@ -295,15 +295,15 @@ f() block = dedent( """ -def f(): - __tracebackhide__ = True - g() + def f(): + __tracebackhide__ = True + g() -def g(): - from IPython.core.debugger import set_trace - set_trace() + def g(): + from IPython.core.debugger import set_trace + set_trace() -f() + f() """ ) @@ -321,3 +321,70 @@ f() child.expect("ipdb>") child.close() + + +@skip_win32 +def test_where_erase_value(): + """Test that `where` does not access f_locals and erase values.""" + import pexpect + + env = os.environ.copy() + env["IPY_TEST_SIMPLE_PROMPT"] = "1" + + child = pexpect.spawn( + sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env + ) + child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE + + child.expect("IPython") + child.expect("\n") + child.expect_exact("In [1]") + + block = dedent( + """ + def simple_f(): + myvar = 1 + print(myvar) + 1/0 + print(myvar) + simple_f() """ + ) + + for line in block.splitlines(): + child.sendline(line) + child.expect_exact(line) + child.expect_exact("ZeroDivisionError") + child.expect_exact("In [2]:") + + child.sendline("%debug") + + ## + child.expect("ipdb>") + + child.sendline("myvar") + child.expect("1") + + ## + child.expect("ipdb>") + + child.sendline("myvar = 2") + + ## + child.expect_exact("ipdb>") + + child.sendline("myvar") + + child.expect_exact("2") + + ## + child.expect("ipdb>") + child.sendline("where") + + ## + child.expect("ipdb>") + child.sendline("myvar") + + child.expect_exact("2") + child.expect("ipdb>") + + child.close() diff --git a/IPython/core/tests/test_display.py b/IPython/core/tests/test_display.py index 8deb11a..24e868f 100644 --- a/IPython/core/tests/test_display.py +++ b/IPython/core/tests/test_display.py @@ -135,7 +135,7 @@ def test_image_filename_defaults(): nt.assert_is_none(img._repr_jpeg_()) def _get_inline_config(): - from ipykernel.pylab.config import InlineBackend + from matplotlib_inline.config import InlineBackend return InlineBackend.instance() @@ -183,7 +183,7 @@ def test_set_matplotlib_formats_kwargs(): ip = get_ipython() cfg = _get_inline_config() cfg.print_figure_kwargs.update(dict(foo='bar')) - kwargs = dict(quality=10) + kwargs = dict(dpi=150) display.set_matplotlib_formats('png', **kwargs) formatter = ip.display_formatter.formatters['image/png'] f = formatter.lookup_by_type(Figure) @@ -457,6 +457,7 @@ def test_display_handle(): 'update': True, }) + def test_image_alt_tag(): """Simple test for display.Image(args, alt=x,)""" thisurl = "http://example.com/image.png" @@ -480,3 +481,8 @@ def test_image_alt_tag(): nt.assert_equal(img.alt, "an image") _, md = img._repr_png_() nt.assert_equal(md["alt"], "an image") + + +@nt.raises(FileNotFoundError) +def test_image_bad_filename_raises_proper_exception(): + display.Image("/this/file/does/not/exist/")._repr_png_() diff --git a/IPython/core/tests/test_formatters.py b/IPython/core/tests/test_formatters.py index cde43c9..15ce8df 100644 --- a/IPython/core/tests/test_formatters.py +++ b/IPython/core/tests/test_formatters.py @@ -48,16 +48,16 @@ def foo_printer(obj, pp, cycle): def test_pretty(): f = PlainTextFormatter() f.for_type(A, foo_printer) - nt.assert_equal(f(A()), 'foo') - nt.assert_equal(f(B()), 'B()') - nt.assert_equal(f(GoodPretty()), 'foo') + assert f(A()) == "foo" + assert f(B()) == "B()" + assert f(GoodPretty()) == "foo" # Just don't raise an exception for the following: f(BadPretty()) f.pprint = False - nt.assert_equal(f(A()), 'A()') - nt.assert_equal(f(B()), 'B()') - nt.assert_equal(f(GoodPretty()), 'GoodPretty()') + assert f(A()) == "A()" + assert f(B()) == "B()" + assert f(GoodPretty()) == "GoodPretty()" def test_deferred(): @@ -66,29 +66,30 @@ def test_deferred(): def test_precision(): """test various values for float_precision.""" f = PlainTextFormatter() - nt.assert_equal(f(pi), repr(pi)) + assert f(pi) == repr(pi) f.float_precision = 0 if numpy: po = numpy.get_printoptions() - nt.assert_equal(po['precision'], 0) - nt.assert_equal(f(pi), '3') + assert po["precision"] == 0 + assert f(pi) == "3" f.float_precision = 2 if numpy: po = numpy.get_printoptions() - nt.assert_equal(po['precision'], 2) - nt.assert_equal(f(pi), '3.14') - f.float_precision = '%g' + assert po["precision"] == 2 + assert f(pi) == "3.14" + f.float_precision = "%g" if numpy: po = numpy.get_printoptions() - nt.assert_equal(po['precision'], 2) - nt.assert_equal(f(pi), '3.14159') - f.float_precision = '%e' - nt.assert_equal(f(pi), '3.141593e+00') - f.float_precision = '' + assert po["precision"] == 2 + assert f(pi) == "3.14159" + f.float_precision = "%e" + assert f(pi) == "3.141593e+00" + f.float_precision = "" if numpy: po = numpy.get_printoptions() - nt.assert_equal(po['precision'], 8) - nt.assert_equal(f(pi), repr(pi)) + assert po["precision"] == 8 + assert f(pi) == repr(pi) + def test_bad_precision(): """test various invalid values for float_precision.""" @@ -260,8 +261,9 @@ def test_nowarn_notimplemented(): with capture_output() as captured: result = f(h) nt.assert_is(result, None) - nt.assert_equal("", captured.stderr) - nt.assert_equal("", captured.stdout) + assert "" == captured.stderr + assert "" == captured.stdout + def test_warn_error_for_type(): f = HTMLFormatter() @@ -307,7 +309,8 @@ class MakePDF(object): def test_pdf_formatter(): pdf = MakePDF() f = PDFFormatter() - nt.assert_equal(f(pdf), 'PDF') + assert f(pdf) == "PDF" + def test_print_method_bound(): f = HTMLFormatter() @@ -321,8 +324,9 @@ def test_print_method_bound(): with capture_output() as captured: result = f(MyHTML()) - nt.assert_equal(result, "hello") - nt.assert_equal(captured.stderr, "") + assert result == "hello" + assert captured.stderr == "" + def test_print_method_weird(): @@ -331,9 +335,9 @@ def test_print_method_weird(): return key f = HTMLFormatter() - + text_hat = TextMagicHat() - nt.assert_equal(text_hat._repr_html_, '_repr_html_') + assert text_hat._repr_html_ == "_repr_html_" with capture_output() as captured: result = f(text_hat) @@ -347,8 +351,8 @@ def test_print_method_weird(): call_hat = CallableMagicHat() with capture_output() as captured: result = f(call_hat) - - nt.assert_equal(result, None) + + assert result is None class BadReprArgs(object): def _repr_html_(self, extra, args): @@ -369,24 +373,25 @@ def test_format_config(): with capture_output() as captured: result = f(cfg) nt.assert_is(result, None) - nt.assert_equal(captured.stderr, "") + assert captured.stderr == "" with capture_output() as captured: result = f(Config) nt.assert_is(result, None) - nt.assert_equal(captured.stderr, "") + assert captured.stderr == "" + def test_pretty_max_seq_length(): f = PlainTextFormatter(max_seq_length=1) lis = list(range(3)) text = f(lis) - nt.assert_equal(text, '[0, ...]') + assert text == "[0, ...]" f.max_seq_length = 0 text = f(lis) - nt.assert_equal(text, '[0, 1, 2]') + assert text == "[0, 1, 2]" text = f(list(range(1024))) lines = text.splitlines() - nt.assert_equal(len(lines), 1024) + assert len(lines) == 1024 def test_ipython_display_formatter(): @@ -409,16 +414,16 @@ def test_ipython_display_formatter(): yes = SelfDisplaying() no = NotSelfDisplaying() - + d, md = f.format(no) - nt.assert_equal(d, {'text/plain': repr(no)}) - nt.assert_equal(md, {}) - nt.assert_equal(catcher, []) - + assert d == {"text/plain": repr(no)} + assert md == {} + assert catcher == [] + d, md = f.format(yes) - nt.assert_equal(d, {}) - nt.assert_equal(md, {}) - nt.assert_equal(catcher, [yes]) + assert d == {} + assert md == {} + assert catcher == [yes] f.ipython_display_formatter.enabled = save_enabled @@ -431,8 +436,8 @@ def test_json_as_string_deprecated(): f = JSONFormatter() with warnings.catch_warnings(record=True) as w: d = f(JSONString()) - nt.assert_equal(d, {}) - nt.assert_equal(len(w), 1) + assert d == {} + assert len(w) == 1 def test_repr_mime(): @@ -458,19 +463,22 @@ def test_repr_mime(): obj = HasReprMime() d, md = f.format(obj) html_f.enabled = save_enabled - - nt.assert_equal(sorted(d), ['application/json+test.v2', - 'image/png', - 'plain/text', - 'text/html', - 'text/plain']) - nt.assert_equal(md, {}) - d, md = f.format(obj, include={'image/png'}) - nt.assert_equal(list(d.keys()), ['image/png'], - 'Include should filter out even things from repr_mimebundle') - nt.assert_equal(d['image/png'], 'i-overwrite', '_repr_mimebundle_ take precedence') + assert sorted(d) == [ + "application/json+test.v2", + "image/png", + "plain/text", + "text/html", + "text/plain", + ] + assert md == {} + d, md = f.format(obj, include={"image/png"}) + assert list(d.keys()) == [ + "image/png" + ], "Include should filter out even things from repr_mimebundle" + + assert d["image/png"] == "i-overwrite", "_repr_mimebundle_ take precedence" def test_pass_correct_include_exclude(): @@ -514,13 +522,14 @@ def test_repr_mime_meta(): f = get_ipython().display_formatter obj = HasReprMimeMeta() d, md = f.format(obj) - nt.assert_equal(sorted(d), ['image/png', 'text/plain']) - nt.assert_equal(md, { - 'image/png': { - 'width': 5, - 'height': 10, + assert sorted(d) == ["image/png", "text/plain"] + assert md == { + "image/png": { + "width": 5, + "height": 10, } - }) + } + def test_repr_mime_failure(): class BadReprMime(object): @@ -530,4 +539,4 @@ def test_repr_mime_failure(): f = get_ipython().display_formatter obj = BadReprMime() d, md = f.format(obj) - nt.assert_in('text/plain', d) + assert "text/plain" in d diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index 57266e5..824ba1e 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -160,6 +160,14 @@ def test_extract_hist_ranges(): actual = list(extract_hist_ranges(instr)) nt.assert_equal(actual, expected) + +def test_extract_hist_ranges_empty_str(): + instr = "" + expected = [(0, 1, None)] # 0 == current session, None == to end + actual = list(extract_hist_ranges(instr)) + nt.assert_equal(actual, expected) + + def test_magic_rerun(): """Simple test for %rerun (no args -> rerun last line)""" ip = get_ipython() diff --git a/IPython/core/tests/test_inputtransformer2.py b/IPython/core/tests/test_inputtransformer2.py index 61f183c..e024d7c 100644 --- a/IPython/core/tests/test_inputtransformer2.py +++ b/IPython/core/tests/test_inputtransformer2.py @@ -255,18 +255,18 @@ def test_find_assign_op_dedent(): def test_check_complete(): cc = ipt2.TransformerManager().check_complete - nt.assert_equal(cc("a = 1"), ('complete', None)) - nt.assert_equal(cc("for a in range(5):"), ('incomplete', 4)) - nt.assert_equal(cc("for a in range(5):\n if a > 0:"), ('incomplete', 8)) - nt.assert_equal(cc("raise = 2"), ('invalid', None)) - nt.assert_equal(cc("a = [1,\n2,"), ('incomplete', 0)) - nt.assert_equal(cc(")"), ('incomplete', 0)) - nt.assert_equal(cc("\\\r\n"), ('incomplete', 0)) - nt.assert_equal(cc("a = '''\n hi"), ('incomplete', 3)) - nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None)) - nt.assert_equal(cc("a \\ "), ('invalid', None)) # Nothing allowed after backslash - nt.assert_equal(cc("1\\\n+2"), ('complete', None)) - nt.assert_equal(cc("exit"), ('complete', None)) + nt.assert_equal(cc("a = 1"), ("complete", None)) + nt.assert_equal(cc("for a in range(5):"), ("incomplete", 4)) + nt.assert_equal(cc("for a in range(5):\n if a > 0:"), ("incomplete", 8)) + nt.assert_equal(cc("raise = 2"), ("invalid", None)) + nt.assert_equal(cc("a = [1,\n2,"), ("incomplete", 0)) + nt.assert_equal(cc("(\n))"), ("incomplete", 0)) + nt.assert_equal(cc("\\\r\n"), ("incomplete", 0)) + nt.assert_equal(cc("a = '''\n hi"), ("incomplete", 3)) + nt.assert_equal(cc("def a():\n x=1\n global x"), ("invalid", None)) + nt.assert_equal(cc("a \\ "), ("invalid", None)) # Nothing allowed after backslash + nt.assert_equal(cc("1\\\n+2"), ("complete", None)) + nt.assert_equal(cc("exit"), ("complete", None)) example = dedent(""" if True: @@ -297,6 +297,24 @@ def test_check_complete_II(): nt.assert_equal(cc('''def foo():\n """'''), ('incomplete', 4)) +def test_check_complete_invalidates_sunken_brackets(): + """ + Test that a single line with more closing brackets than the opening ones is + interpretted as invalid + """ + cc = ipt2.TransformerManager().check_complete + nt.assert_equal(cc(")"), ("invalid", None)) + nt.assert_equal(cc("]"), ("invalid", None)) + nt.assert_equal(cc("}"), ("invalid", None)) + nt.assert_equal(cc(")("), ("invalid", None)) + nt.assert_equal(cc("]["), ("invalid", None)) + nt.assert_equal(cc("}{"), ("invalid", None)) + nt.assert_equal(cc("]()("), ("invalid", None)) + nt.assert_equal(cc("())("), ("invalid", None)) + nt.assert_equal(cc(")[]("), ("invalid", None)) + nt.assert_equal(cc("()]("), ("invalid", None)) + + def test_null_cleanup_transformer(): manager = ipt2.TransformerManager() manager.cleanup_transforms.insert(0, null_cleanup_transformer) diff --git a/IPython/core/tests/test_inputtransformer2_line.py b/IPython/core/tests/test_inputtransformer2_line.py index 263bbd9..30558fd 100644 --- a/IPython/core/tests/test_inputtransformer2_line.py +++ b/IPython/core/tests/test_inputtransformer2_line.py @@ -3,7 +3,7 @@ Line-based transformers are the simpler ones; token-based transformers are more complex. See test_inputtransformer2 for tests for token-based transformers. """ -import nose.tools as nt +import pytest from IPython.core import inputtransformer2 as ipt2 @@ -17,8 +17,9 @@ get_ipython().run_cell_magic('foo', 'arg', 'body 1\\nbody 2\\n') def test_cell_magic(): for sample, expected in [CELL_MAGIC]: - nt.assert_equal(ipt2.cell_magic(sample.splitlines(keepends=True)), - expected.splitlines(keepends=True)) + assert ipt2.cell_magic(sample.splitlines(keepends=True)) == expected.splitlines( + keepends=True + ) CLASSIC_PROMPT = ("""\ >>> for a in range(5): @@ -40,8 +41,9 @@ for a in range(5): def test_classic_prompt(): for sample, expected in [CLASSIC_PROMPT, CLASSIC_PROMPT_L2]: - nt.assert_equal(ipt2.classic_prompt(sample.splitlines(keepends=True)), - expected.splitlines(keepends=True)) + assert ipt2.classic_prompt( + sample.splitlines(keepends=True) + ) == expected.splitlines(keepends=True) IPYTHON_PROMPT = ("""\ In [1]: for a in range(5): @@ -61,10 +63,49 @@ for a in range(5): print(a ** 2) """) + +IPYTHON_PROMPT_VI_INS = ( + """\ +[ins] In [11]: def a(): + ...: 123 + ...: + ...: 123 +""", + """\ +def a(): + 123 + +123 +""", +) + +IPYTHON_PROMPT_VI_NAV = ( + """\ +[nav] In [11]: def a(): + ...: 123 + ...: + ...: 123 +""", + """\ +def a(): + 123 + +123 +""", +) + + def test_ipython_prompt(): - for sample, expected in [IPYTHON_PROMPT, IPYTHON_PROMPT_L2]: - nt.assert_equal(ipt2.ipython_prompt(sample.splitlines(keepends=True)), - expected.splitlines(keepends=True)) + for sample, expected in [ + IPYTHON_PROMPT, + IPYTHON_PROMPT_L2, + IPYTHON_PROMPT_VI_INS, + IPYTHON_PROMPT_VI_NAV, + ]: + assert ipt2.ipython_prompt( + sample.splitlines(keepends=True) + ) == expected.splitlines(keepends=True) + INDENT_SPACES = ("""\ if True: @@ -84,8 +125,9 @@ if True: def test_leading_indent(): for sample, expected in [INDENT_SPACES, INDENT_TABS]: - nt.assert_equal(ipt2.leading_indent(sample.splitlines(keepends=True)), - expected.splitlines(keepends=True)) + assert ipt2.leading_indent( + sample.splitlines(keepends=True) + ) == expected.splitlines(keepends=True) LEADING_EMPTY_LINES = ("""\ \t @@ -111,9 +153,9 @@ ONLY_EMPTY_LINES = ("""\ def test_leading_empty_lines(): for sample, expected in [LEADING_EMPTY_LINES, ONLY_EMPTY_LINES]: - nt.assert_equal( - ipt2.leading_empty_lines(sample.splitlines(keepends=True)), - expected.splitlines(keepends=True)) + assert ipt2.leading_empty_lines( + sample.splitlines(keepends=True) + ) == expected.splitlines(keepends=True) CRLF_MAGIC = ([ "%%ls\r\n" @@ -123,4 +165,4 @@ CRLF_MAGIC = ([ def test_crlf_magic(): for sample, expected in [CRLF_MAGIC]: - nt.assert_equal(ipt2.cell_magic(sample), expected) \ No newline at end of file + assert ipt2.cell_magic(sample) == expected diff --git a/IPython/core/tests/test_interactiveshell.py b/IPython/core/tests/test_interactiveshell.py index 951b843..9092ce5 100644 --- a/IPython/core/tests/test_interactiveshell.py +++ b/IPython/core/tests/test_interactiveshell.py @@ -449,6 +449,25 @@ class InteractiveShellTestCase(unittest.TestCase): # Reset the custom exception hook ip.set_custom_exc((), None) + @mock.patch("builtins.print") + def test_showtraceback_with_surrogates(self, mocked_print): + values = [] + + def mock_print_func(value, sep=" ", end="\n", file=sys.stdout, flush=False): + values.append(value) + if value == chr(0xD8FF): + raise UnicodeEncodeError("utf-8", chr(0xD8FF), 0, 1, "") + + # mock builtins.print + mocked_print.side_effect = mock_print_func + + # ip._showtraceback() is replaced in globalipapp.py. + # Call original method to test. + interactiveshell.InteractiveShell._showtraceback(ip, None, None, chr(0xD8FF)) + + self.assertEqual(mocked_print.call_count, 2) + self.assertEqual(values, [chr(0xD8FF), "\\ud8ff"]) + def test_mktempfile(self): filename = ip.mktempfile() # Check that we can open the file again on Windows @@ -581,9 +600,16 @@ class TestSystemRaw(ExitCodeChecks): try: self.system("sleep 1 # wont happen") except KeyboardInterrupt: - self.fail("system call should intercept " - "keyboard interrupt from subprocess.call") - self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGINT) + self.fail( + "system call should intercept " + "keyboard interrupt from subprocess.call" + ) + self.assertEqual(ip.user_ns["_exit_code"], -signal.SIGINT) + + def test_magic_warnings(self): + for magic_cmd in ("ls", "pip", "conda", "cd"): + with self.assertWarnsRegex(Warning, "You executed the system command"): + ip.system_raw(magic_cmd) # TODO: Exit codes are currently ignored on Windows. class TestSystemPipedExitCode(ExitCodeChecks): diff --git a/IPython/core/tests/test_iplib.py b/IPython/core/tests/test_iplib.py index 52fafca..6804c2b 100644 --- a/IPython/core/tests/test_iplib.py +++ b/IPython/core/tests/test_iplib.py @@ -5,7 +5,7 @@ #----------------------------------------------------------------------------- # third party -import nose.tools as nt +import pytest # our own packages @@ -31,8 +31,8 @@ def test_reset(): # Finally, check that all namespaces have only as many variables as we # expect to find in them: - nt.assert_equal(len(ip.user_ns), nvars_user_ns) - nt.assert_equal(len(ip.user_ns_hidden), nvars_hidden) + assert len(ip.user_ns) == nvars_user_ns + assert len(ip.user_ns_hidden) == nvars_hidden # Tests for reporting of exceptions in various modes, handling of SystemExit, @@ -87,123 +87,133 @@ ZeroDivisionError: ... def doctest_tb_verbose(): """ -In [5]: xmode verbose -Exception reporting mode: Verbose - -In [6]: run simpleerr.py ---------------------------------------------------------------------------- -ZeroDivisionError Traceback (most recent call last) - -... in - 29 except IndexError: - 30 mode = 'div' ----> 32 bar(mode) - mode = 'div' - -... in bar(mode='div') - 14 "bar" - 15 if mode=='div': ----> 16 div0() - 17 elif mode=='exit': - 18 try: - -... in div0() - 6 x = 1 - 7 y = 0 -----> 8 x/y - x = 1 - y = 0 - -ZeroDivisionError: ... - """ - -def doctest_tb_sysexit(): + In [5]: xmode verbose + Exception reporting mode: Verbose + + In [6]: run simpleerr.py + --------------------------------------------------------------------------- + ZeroDivisionError Traceback (most recent call last) + + ... in + 29 except IndexError: + 30 mode = 'div' + ---> 32 bar(mode) + mode = 'div' + + ... in bar(mode='div') + 14 "bar" + 15 if mode=='div': + ---> 16 div0() + 17 elif mode=='exit': + 18 try: + + ... in div0() + 6 x = 1 + 7 y = 0 + ----> 8 x/y + x = 1 + y = 0 + + ZeroDivisionError: ... """ -In [17]: %xmode plain -Exception reporting mode: Plain - -In [18]: %run simpleerr.py exit -An exception has occurred, use %tb to see the full traceback. -SystemExit: (1, 'Mode = exit') - -In [19]: %run simpleerr.py exit 2 -An exception has occurred, use %tb to see the full traceback. -SystemExit: (2, 'Mode = exit') -In [20]: %tb -Traceback (most recent call last): - File ... in - bar(mode) - File ... line 22, in bar - sysexit(stat, mode) - File ... line 11, in sysexit - raise SystemExit(stat, 'Mode = %s' % mode) -SystemExit: (2, 'Mode = exit') -In [21]: %xmode context -Exception reporting mode: Context - -In [22]: %tb ---------------------------------------------------------------------------- -SystemExit Traceback (most recent call last) - -... - 29 except IndexError: - 30 mode = 'div' ----> 32 bar(mode) - -...bar(mode) - 20 except: - 21 stat = 1 ----> 22 sysexit(stat, mode) - 23 else: - 24 raise ValueError('Unknown mode') - -...sysexit(stat, mode) - 10 def sysexit(stat, mode): ----> 11 raise SystemExit(stat, 'Mode = %s' % mode) - -SystemExit: (2, 'Mode = exit') - -In [23]: %xmode verbose -Exception reporting mode: Verbose - -In [24]: %tb ---------------------------------------------------------------------------- -SystemExit Traceback (most recent call last) - -... in - 29 except IndexError: - 30 mode = 'div' ----> 32 bar(mode) - mode = 'exit' - -... in bar(mode='exit') - 20 except: - 21 stat = 1 ----> 22 sysexit(stat, mode) - mode = 'exit' - stat = 2 - 23 else: - 24 raise ValueError('Unknown mode') - -... in sysexit(stat=2, mode='exit') - 10 def sysexit(stat, mode): ----> 11 raise SystemExit(stat, 'Mode = %s' % mode) - stat = 2 - mode = 'exit' - -SystemExit: (2, 'Mode = exit') - """ +# TODO : Marc 2021 – this seem to fail due +# to upstream changes in CI for whatever reason. +# Commenting for now, to revive someday (maybe?) +# nose won't work in 3.10 anyway and we'll have to disable iptest. +# thus this likely need to bemigrated to pytest. + + +# def doctest_tb_sysexit(): +# """ +# In [17]: %xmode plain +# Exception reporting mode: Plain +# +# In [18]: %run simpleerr.py exit +# An exception has occurred, use %tb to see the full traceback. +# SystemExit: (1, 'Mode = exit') +# +# In [19]: %run simpleerr.py exit 2 +# An exception has occurred, use %tb to see the full traceback. +# SystemExit: (2, 'Mode = exit') +# +# In [20]: %tb +# Traceback (most recent call last): +# File ... in +# bar(mode) +# File ... line 22, in bar +# sysexit(stat, mode) +# File ... line 11, in sysexit +# raise SystemExit(stat, 'Mode = %s' % mode) +# SystemExit: (2, 'Mode = exit') +# +# In [21]: %xmode context +# Exception reporting mode: Context +# +# In [22]: %tb +# --------------------------------------------------------------------------- +# SystemExit Traceback (most recent call last) +# +# ... +# 29 except IndexError: +# 30 mode = 'div' +# ---> 32 bar(mode) +# +# ...bar(mode) +# 20 except: +# 21 stat = 1 +# ---> 22 sysexit(stat, mode) +# 23 else: +# 24 raise ValueError('Unknown mode') +# +# ...sysexit(stat, mode) +# 10 def sysexit(stat, mode): +# ---> 11 raise SystemExit(stat, 'Mode = %s' % mode) +# +# SystemExit: (2, 'Mode = exit') +# +# In [23]: %xmode verbose +# Exception reporting mode: Verbose +# +# In [24]: %tb +# --------------------------------------------------------------------------- +# SystemExit Traceback (most recent call last) +# +# ... in +# 29 except IndexError: +# 30 mode = 'div' +# ---> 32 bar(mode) +# mode = 'exit' +# +# ... in bar(mode='exit') +# 20 except: +# 21 stat = 1 +# ---> 22 sysexit(stat, mode) +# mode = 'exit' +# stat = 2 +# 23 else: +# 24 raise ValueError('Unknown mode') +# +# ... in sysexit(stat=2, mode='exit') +# 10 def sysexit(stat, mode): +# ---> 11 raise SystemExit(stat, 'Mode = %s' % mode) +# stat = 2 +# mode = 'exit' +# +# SystemExit: (2, 'Mode = exit') +# """ def test_run_cell(): import textwrap - ip.run_cell('a = 10\na+=1') - ip.run_cell('assert a == 11\nassert 1') - nt.assert_equal(ip.user_ns['a'], 11) - complex = textwrap.dedent(""" + ip.run_cell("a = 10\na+=1") + ip.run_cell("assert a == 11\nassert 1") + + assert ip.user_ns["a"] == 11 + complex = textwrap.dedent( + """ if 1: print "hello" if 1: @@ -225,7 +235,7 @@ def test_run_cell(): def test_db(): """Test the internal database used for variable persistence.""" - ip.db['__unittest_'] = 12 - nt.assert_equal(ip.db['__unittest_'], 12) - del ip.db['__unittest_'] - assert '__unittest_' not in ip.db + ip.db["__unittest_"] = 12 + assert ip.db["__unittest_"] == 12 + del ip.db["__unittest_"] + assert "__unittest_" not in ip.db diff --git a/IPython/core/tests/test_logger.py b/IPython/core/tests/test_logger.py index ebebac1..e53385f 100644 --- a/IPython/core/tests/test_logger.py +++ b/IPython/core/tests/test_logger.py @@ -2,8 +2,8 @@ """Test IPython.core.logger""" import os.path +import pytest -import nose.tools as nt from IPython.utils.tempdir import TemporaryDirectory def test_logstart_inaccessible_file(): @@ -12,8 +12,8 @@ def test_logstart_inaccessible_file(): except IOError: pass else: - nt.assert_true(False) # The try block should never pass. - + assert False # The try block should never pass. + try: _ip.run_cell("a=1") # Check it doesn't try to log this finally: diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 66ba309..fd5696d 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -472,6 +472,23 @@ def test_parse_options(): nt.assert_equal(m.parse_options(u'foo', '')[1], u'foo') +def test_parse_options_preserve_non_option_string(): + """Test to assert preservation of non-option part of magic-block, while parsing magic options.""" + m = DummyMagics(_ip) + opts, stmt = m.parse_options( + " -n1 -r 13 _ = 314 + foo", "n:r:", preserve_non_opts=True + ) + nt.assert_equal(opts, {"n": "1", "r": "13"}) + nt.assert_equal(stmt, "_ = 314 + foo") + + +def test_run_magic_preserve_code_block(): + """Test to assert preservation of non-option part of magic-block, while running magic.""" + _ip.user_ns["spaces"] = [] + _ip.magic("timeit -n1 -r1 spaces.append([s.count(' ') for s in ['document']])") + assert _ip.user_ns["spaces"] == [[0]] + + def test_dirops(): """Test various directory handling operations.""" # curpath = lambda :os.path.splitdrive(os.getcwd())[1].replace('\\','/') @@ -1072,6 +1089,29 @@ def test_save(): nt.assert_in("coding: utf-8", content) +def test_save_with_no_args(): + ip = get_ipython() + ip.history_manager.reset() # Clear any existing history. + cmds = [u"a=1", u"def b():\n return a**2", u"print(a, b())", "%save"] + for i, cmd in enumerate(cmds, start=1): + ip.history_manager.store_inputs(i, cmd) + + with TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "testsave.py") + ip.run_line_magic("save", path) + content = Path(path).read_text() + expected_content = dedent( + """\ + # coding: utf-8 + a=1 + def b(): + return a**2 + print(a, b()) + """ + ) + nt.assert_equal(content, expected_content) + + def test_store(): """Test %store.""" ip = get_ipython() diff --git a/IPython/core/tests/test_magic_arguments.py b/IPython/core/tests/test_magic_arguments.py index 5dea32d..62946dc 100644 --- a/IPython/core/tests/test_magic_arguments.py +++ b/IPython/core/tests/test_magic_arguments.py @@ -7,7 +7,7 @@ #----------------------------------------------------------------------------- import argparse -from nose.tools import assert_equal +import pytest from IPython.core.magic_arguments import (argument, argument_group, kwds, magic_arguments, parse_argstring, real_name) @@ -74,45 +74,62 @@ def foo(self, args): def test_magic_arguments(): - assert_equal(magic_foo1.__doc__, '::\n\n %foo1 [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') - assert_equal(getattr(magic_foo1, 'argcmd_name', None), None) - assert_equal(real_name(magic_foo1), 'foo1') - assert_equal(magic_foo1(None, ''), argparse.Namespace(foo=None)) - assert hasattr(magic_foo1, 'has_arguments') - - assert_equal(magic_foo2.__doc__, '::\n\n %foo2\n\n A docstring.\n') - assert_equal(getattr(magic_foo2, 'argcmd_name', None), None) - assert_equal(real_name(magic_foo2), 'foo2') - assert_equal(magic_foo2(None, ''), argparse.Namespace()) - assert hasattr(magic_foo2, 'has_arguments') - - assert_equal(magic_foo3.__doc__, '::\n\n %foo3 [-f FOO] [-b BAR] [-z BAZ]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n\nGroup:\n -b BAR, --bar BAR a grouped argument\n\nSecond Group:\n -z BAZ, --baz BAZ another grouped argument\n') - assert_equal(getattr(magic_foo3, 'argcmd_name', None), None) - assert_equal(real_name(magic_foo3), 'foo3') - assert_equal(magic_foo3(None, ''), - argparse.Namespace(bar=None, baz=None, foo=None)) - assert hasattr(magic_foo3, 'has_arguments') - - assert_equal(magic_foo4.__doc__, '::\n\n %foo4 [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') - assert_equal(getattr(magic_foo4, 'argcmd_name', None), None) - assert_equal(real_name(magic_foo4), 'foo4') - assert_equal(magic_foo4(None, ''), argparse.Namespace()) - assert hasattr(magic_foo4, 'has_arguments') - - assert_equal(magic_foo5.__doc__, '::\n\n %frobnicate [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') - assert_equal(getattr(magic_foo5, 'argcmd_name', None), 'frobnicate') - assert_equal(real_name(magic_foo5), 'frobnicate') - assert_equal(magic_foo5(None, ''), argparse.Namespace(foo=None)) - assert hasattr(magic_foo5, 'has_arguments') - - assert_equal(magic_magic_foo.__doc__, '::\n\n %magic_foo [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') - assert_equal(getattr(magic_magic_foo, 'argcmd_name', None), None) - assert_equal(real_name(magic_magic_foo), 'magic_foo') - assert_equal(magic_magic_foo(None, ''), argparse.Namespace(foo=None)) - assert hasattr(magic_magic_foo, 'has_arguments') - - assert_equal(foo.__doc__, '::\n\n %foo [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n') - assert_equal(getattr(foo, 'argcmd_name', None), None) - assert_equal(real_name(foo), 'foo') - assert_equal(foo(None, ''), argparse.Namespace(foo=None)) - assert hasattr(foo, 'has_arguments') + assert ( + magic_foo1.__doc__ + == "::\n\n %foo1 [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n" + ) + assert getattr(magic_foo1, "argcmd_name", None) == None + assert real_name(magic_foo1) == "foo1" + assert magic_foo1(None, "") == argparse.Namespace(foo=None) + assert hasattr(magic_foo1, "has_arguments") + + assert magic_foo2.__doc__ == "::\n\n %foo2\n\n A docstring.\n" + assert getattr(magic_foo2, "argcmd_name", None) == None + assert real_name(magic_foo2) == "foo2" + assert magic_foo2(None, "") == argparse.Namespace() + assert hasattr(magic_foo2, "has_arguments") + + assert ( + magic_foo3.__doc__ + == "::\n\n %foo3 [-f FOO] [-b BAR] [-z BAZ]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n\nGroup:\n -b BAR, --bar BAR a grouped argument\n\nSecond Group:\n -z BAZ, --baz BAZ another grouped argument\n" + ) + assert getattr(magic_foo3, "argcmd_name", None) == None + assert real_name(magic_foo3) == "foo3" + assert magic_foo3(None, "") == argparse.Namespace(bar=None, baz=None, foo=None) + assert hasattr(magic_foo3, "has_arguments") + + assert ( + magic_foo4.__doc__ + == "::\n\n %foo4 [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n" + ) + assert getattr(magic_foo4, "argcmd_name", None) == None + assert real_name(magic_foo4) == "foo4" + assert magic_foo4(None, "") == argparse.Namespace() + assert hasattr(magic_foo4, "has_arguments") + + assert ( + magic_foo5.__doc__ + == "::\n\n %frobnicate [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n" + ) + assert getattr(magic_foo5, "argcmd_name", None) == "frobnicate" + assert real_name(magic_foo5) == "frobnicate" + assert magic_foo5(None, "") == argparse.Namespace(foo=None) + assert hasattr(magic_foo5, "has_arguments") + + assert ( + magic_magic_foo.__doc__ + == "::\n\n %magic_foo [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n" + ) + assert getattr(magic_magic_foo, "argcmd_name", None) == None + assert real_name(magic_magic_foo) == "magic_foo" + assert magic_magic_foo(None, "") == argparse.Namespace(foo=None) + assert hasattr(magic_magic_foo, "has_arguments") + + assert ( + foo.__doc__ + == "::\n\n %foo [-f FOO]\n\n A docstring.\n\noptional arguments:\n -f FOO, --foo FOO an argument\n" + ) + assert getattr(foo, "argcmd_name", None) == None + assert real_name(foo) == "foo" + assert foo(None, "") == argparse.Namespace(foo=None) + assert hasattr(foo, "has_arguments") diff --git a/IPython/core/tests/test_oinspect.py b/IPython/core/tests/test_oinspect.py index 19c6db7..6d3ae2e 100644 --- a/IPython/core/tests/test_oinspect.py +++ b/IPython/core/tests/test_oinspect.py @@ -56,7 +56,7 @@ def pyfile(fname): def match_pyfiles(f1, f2): - nt.assert_equal(pyfile(f1), pyfile(f2)) + assert pyfile(f1) == pyfile(f2) def test_find_file(): @@ -76,7 +76,7 @@ def test_find_file_decorated1(): "My docstring" match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) - nt.assert_equal(f.__doc__, "My docstring") + assert f.__doc__ == "My docstring" def test_find_file_decorated2(): @@ -92,7 +92,7 @@ def test_find_file_decorated2(): "My docstring 2" match_pyfiles(oinspect.find_file(f), os.path.abspath(__file__)) - nt.assert_equal(f.__doc__, "My docstring 2") + assert f.__doc__ == "My docstring 2" def test_find_file_magic(): @@ -167,41 +167,46 @@ class SerialLiar(object): def test_info(): "Check that Inspector.info fills out various fields as expected." - i = inspector.info(Call, oname='Call') - nt.assert_equal(i['type_name'], 'type') + i = inspector.info(Call, oname="Call") + assert i["type_name"] == "type" expted_class = str(type(type)) # (Python 3) or - nt.assert_equal(i['base_class'], expted_class) - nt.assert_regex(i['string_form'], "") + assert i["base_class"] == expted_class + nt.assert_regex( + i["string_form"], + "", + ) fname = __file__ if fname.endswith(".pyc"): fname = fname[:-1] # case-insensitive comparison needed on some filesystems # e.g. Windows: - nt.assert_equal(i['file'].lower(), compress_user(fname).lower()) - nt.assert_equal(i['definition'], None) - nt.assert_equal(i['docstring'], Call.__doc__) - nt.assert_equal(i['source'], None) - nt.assert_true(i['isclass']) - nt.assert_equal(i['init_definition'], "Call(x, y=1)") - nt.assert_equal(i['init_docstring'], Call.__init__.__doc__) + assert i["file"].lower() == compress_user(fname).lower() + assert i["definition"] == None + assert i["docstring"] == Call.__doc__ + assert i["source"] == None + nt.assert_true(i["isclass"]) + assert i["init_definition"] == "Call(x, y=1)" + assert i["init_docstring"] == Call.__init__.__doc__ i = inspector.info(Call, detail_level=1) - nt.assert_not_equal(i['source'], None) - nt.assert_equal(i['docstring'], None) + nt.assert_not_equal(i["source"], None) + assert i["docstring"] == None c = Call(1) c.__doc__ = "Modified instance docstring" i = inspector.info(c) - nt.assert_equal(i['type_name'], 'Call') - nt.assert_equal(i['docstring'], "Modified instance docstring") - nt.assert_equal(i['class_docstring'], Call.__doc__) - nt.assert_equal(i['init_docstring'], Call.__init__.__doc__) - nt.assert_equal(i['call_docstring'], Call.__call__.__doc__) + assert i["type_name"] == "Call" + assert i["docstring"] == "Modified instance docstring" + assert i["class_docstring"] == Call.__doc__ + assert i["init_docstring"] == Call.__init__.__doc__ + assert i["call_docstring"] == Call.__call__.__doc__ + def test_class_signature(): - info = inspector.info(HasSignature, 'HasSignature') - nt.assert_equal(info['init_definition'], "HasSignature(test)") - nt.assert_equal(info['init_docstring'], HasSignature.__init__.__doc__) + info = inspector.info(HasSignature, "HasSignature") + assert info["init_definition"] == "HasSignature(test)" + assert info["init_docstring"] == HasSignature.__init__.__doc__ + def test_info_awkward(): # Just test that this doesn't throw an error. @@ -231,8 +236,9 @@ def f_kwarg(pos, *, kwonly): pass def test_definition_kwonlyargs(): - i = inspector.info(f_kwarg, oname='f_kwarg') # analysis:ignore - nt.assert_equal(i['definition'], "f_kwarg(pos, *, kwonly)") + i = inspector.info(f_kwarg, oname="f_kwarg") # analysis:ignore + assert i["definition"] == "f_kwarg(pos, *, kwonly)" + def test_getdoc(): class A(object): @@ -253,9 +259,9 @@ def test_getdoc(): b = B() c = C() - nt.assert_equal(oinspect.getdoc(a), "standard docstring") - nt.assert_equal(oinspect.getdoc(b), "custom docstring") - nt.assert_equal(oinspect.getdoc(c), "standard docstring") + assert oinspect.getdoc(a) == "standard docstring" + assert oinspect.getdoc(b) == "custom docstring" + assert oinspect.getdoc(c) == "standard docstring" def test_empty_property_has_no_source(): @@ -299,15 +305,17 @@ def test_property_docstring_is_in_info_for_detail_level_0(): """This is `foobar` property.""" pass - ip.user_ns['a_obj'] = A() - nt.assert_equal( - 'This is `foobar` property.', - ip.object_inspect('a_obj.foobar', detail_level=0)['docstring']) + ip.user_ns["a_obj"] = A() + assert ( + "This is `foobar` property." + == ip.object_inspect("a_obj.foobar", detail_level=0)["docstring"] + ) - ip.user_ns['a_cls'] = A - nt.assert_equal( - 'This is `foobar` property.', - ip.object_inspect('a_cls.foobar', detail_level=0)['docstring']) + ip.user_ns["a_cls"] = A + assert ( + "This is `foobar` property." + == ip.object_inspect("a_cls.foobar", detail_level=0)["docstring"] + ) def test_pdef(): @@ -404,7 +412,7 @@ def test_render_signature_short(): signature(short_fun), short_fun.__name__, ) - nt.assert_equal(sig, 'short_fun(a=1)') + assert sig == "short_fun(a=1)" def test_render_signature_long(): diff --git a/IPython/core/tests/test_prefilter.py b/IPython/core/tests/test_prefilter.py index ca447b3..91c3c86 100644 --- a/IPython/core/tests/test_prefilter.py +++ b/IPython/core/tests/test_prefilter.py @@ -3,7 +3,7 @@ #----------------------------------------------------------------------------- # Imports #----------------------------------------------------------------------------- -import nose.tools as nt +import pytest from IPython.core.prefilter import AutocallChecker @@ -19,7 +19,7 @@ def test_prefilter(): ] for raw, correct in pairs: - nt.assert_equal(ip.prefilter(raw), correct) + assert ip.prefilter(raw) == correct def test_prefilter_shadowed(): def dummy_magic(line): pass @@ -32,16 +32,16 @@ def test_prefilter_shadowed(): # These should not be transformed - they are shadowed by other names for name in ['if', 'zip', 'get_ipython']: # keyword, builtin, global ip.register_magic_function(dummy_magic, magic_name=name) - res = ip.prefilter(name+' foo') - nt.assert_equal(res, name+' foo') - del ip.magics_manager.magics['line'][name] + res = ip.prefilter(name + " foo") + assert res == name + " foo" + del ip.magics_manager.magics["line"][name] # These should be transformed for name in ['fi', 'piz', 'nohtypi_teg']: ip.register_magic_function(dummy_magic, magic_name=name) - res = ip.prefilter(name+' foo') - nt.assert_not_equal(res, name+' foo') - del ip.magics_manager.magics['line'][name] + res = ip.prefilter(name + " foo") + assert res != name + " foo" + del ip.magics_manager.magics["line"][name] finally: ip.automagic = prev_automagic_state @@ -52,9 +52,9 @@ def test_autocall_binops(): f = lambda x: x ip.user_ns['f'] = f try: - nt.assert_equal(ip.prefilter('f 1'),'f(1)') - for t in ['f +1', 'f -1']: - nt.assert_equal(ip.prefilter(t), t) + assert ip.prefilter("f 1") == "f(1)" + for t in ["f +1", "f -1"]: + assert ip.prefilter(t) == t # Run tests again with a more permissive exclude_regexp, which will # allow transformation of binary operations ('f -1' -> 'f(-1)'). @@ -66,8 +66,8 @@ def test_autocall_binops(): ac.exclude_regexp = r'^[,&^\|\*/]|^is |^not |^in |^and |^or ' pm.sort_checkers() - nt.assert_equal(ip.prefilter('f -1'), 'f(-1)') - nt.assert_equal(ip.prefilter('f +1'), 'f(+1)') + assert ip.prefilter("f -1") == "f(-1)" + assert ip.prefilter("f +1") == "f(+1)" finally: pm.unregister_checker(ac) finally: @@ -88,7 +88,7 @@ def test_issue_114(): try: for mgk in ip.magics_manager.lsmagic()['line']: raw = template % mgk - nt.assert_equal(ip.prefilter(raw), raw) + assert ip.prefilter(raw) == raw finally: ip.prefilter_manager.multi_line_specials = msp @@ -121,7 +121,7 @@ def test_autocall_should_support_unicode(): ip.magic('autocall 2') ip.user_ns['π'] = lambda x: x try: - nt.assert_equal(ip.prefilter('π 3'),'π(3)') + assert ip.prefilter("π 3") == "π(3)" finally: ip.magic('autocall 0') del ip.user_ns['π'] diff --git a/IPython/core/tests/test_pylabtools.py b/IPython/core/tests/test_pylabtools.py index 75b103f..9a6b2f7 100644 --- a/IPython/core/tests/test_pylabtools.py +++ b/IPython/core/tests/test_pylabtools.py @@ -15,6 +15,7 @@ from nose import SkipTest import nose.tools as nt from matplotlib import pyplot as plt +import matplotlib_inline import numpy as np from IPython.core.getipython import get_ipython @@ -146,8 +147,16 @@ def test_import_pylab(): nt.assert_true('plt' in ns) nt.assert_equal(ns['np'], np) +from traitlets.config import Config + + class TestPylabSwitch(object): class Shell(InteractiveShell): + def init_history(self): + """Sets up the command history, and starts regular autosaves.""" + self.config.HistoryManager.hist_file = ":memory:" + super().init_history() + def enable_gui(self, gui): pass @@ -167,18 +176,21 @@ class TestPylabSwitch(object): pt.activate_matplotlib = act_mpl self._save_ip = pt.import_pylab pt.import_pylab = lambda *a,**kw:None - self._save_cis = pt.configure_inline_support - pt.configure_inline_support = lambda *a,**kw:None + self._save_cis = matplotlib_inline.backend_inline.configure_inline_support + matplotlib_inline.backend_inline.configure_inline_support = ( + lambda *a, **kw: None + ) def teardown(self): pt.activate_matplotlib = self._save_am pt.import_pylab = self._save_ip - pt.configure_inline_support = self._save_cis + matplotlib_inline.backend_inline.configure_inline_support = self._save_cis import matplotlib matplotlib.rcParams = self._saved_rcParams matplotlib.rcParamsOrig = self._saved_rcParamsOrig def test_qt(self): + s = self.Shell() gui, backend = s.enable_matplotlib(None) nt.assert_equal(gui, 'qt') diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index beed8c0..c7ce562 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -160,7 +160,7 @@ def _format_traceback_lines(lines, Colors, has_colors, lvals): else: num = '%*s' % (numbers_width, lineno) start_color = Colors.lineno - + line = '%s%s%s %s' % (start_color, num, Colors.Normal, line) res.append(line) @@ -169,6 +169,31 @@ def _format_traceback_lines(lines, Colors, has_colors, lvals): return res +def _format_filename(file, ColorFilename, ColorNormal): + """ + Format filename lines with `In [n]` if it's the nth code cell or `File *.py` if it's a module. + + Parameters + ---------- + file : str + ColorFilename + ColorScheme's filename coloring to be used. + ColorNormal + ColorScheme's normal coloring to be used. + """ + ipinst = get_ipython() + + if ipinst is not None and file in ipinst.compile._filename_map: + file = "[%s]" % ipinst.compile._filename_map[file] + tpl_link = "In %s%%s%s" % (ColorFilename, ColorNormal) + else: + file = util_path.compress_user( + py3compat.cast_unicode(file, util_path.fs_encoding) + ) + tpl_link = "File %s%%s%s" % (ColorFilename, ColorNormal) + + return tpl_link % file + #--------------------------------------------------------------------------- # Module classes class TBTools(colorable.Colorable): @@ -300,7 +325,7 @@ class ListTB(TBTools): Calling requires 3 arguments: (etype, evalue, elist) as would be obtained by:: - + etype, evalue, tb = sys.exc_info() if tb: elist = traceback.extract_tb(tb) @@ -414,21 +439,31 @@ class ListTB(TBTools): Colors = self.Colors list = [] for filename, lineno, name, line in extracted_list[:-1]: - item = ' File %s"%s"%s, line %s%d%s, in %s%s%s\n' % \ - (Colors.filename, filename, Colors.Normal, - Colors.lineno, lineno, Colors.Normal, - Colors.name, name, Colors.Normal) + item = " %s, line %s%d%s, in %s%s%s\n" % ( + _format_filename(filename, Colors.filename, Colors.Normal), + Colors.lineno, + lineno, + Colors.Normal, + Colors.name, + name, + Colors.Normal, + ) if line: item += ' %s\n' % line.strip() list.append(item) # Emphasize the last entry filename, lineno, name, line = extracted_list[-1] - item = '%s File %s"%s"%s, line %s%d%s, in %s%s%s%s\n' % \ - (Colors.normalEm, - Colors.filenameEm, filename, Colors.normalEm, - Colors.linenoEm, lineno, Colors.normalEm, - Colors.nameEm, name, Colors.normalEm, - Colors.Normal) + item = "%s %s, line %s%d%s, in %s%s%s%s\n" % ( + Colors.normalEm, + _format_filename(filename, Colors.filenameEm, Colors.normalEm), + Colors.linenoEm, + lineno, + Colors.normalEm, + Colors.nameEm, + name, + Colors.normalEm, + Colors.Normal, + ) if line: item += '%s %s%s\n' % (Colors.line, line.strip(), Colors.Normal) @@ -463,13 +498,21 @@ class ListTB(TBTools): lineno = value.lineno textline = linecache.getline(value.filename, value.lineno) else: - lineno = 'unknown' - textline = '' - list.append('%s File %s"%s"%s, line %s%s%s\n' % \ - (Colors.normalEm, - Colors.filenameEm, py3compat.cast_unicode(value.filename), Colors.normalEm, - Colors.linenoEm, lineno, Colors.Normal )) - if textline == '': + lineno = "unknown" + textline = "" + list.append( + "%s %s, line %s%s%s\n" + % ( + Colors.normalEm, + _format_filename( + value.filename, Colors.filenameEm, Colors.normalEm + ), + Colors.linenoEm, + lineno, + Colors.Normal, + ) + ) + if textline == "": textline = py3compat.cast_unicode(value.text, "utf-8") if textline is not None: @@ -581,25 +624,19 @@ class VerboseTB(TBTools): Colors = self.Colors # just a shorthand + quicker name lookup ColorsNormal = Colors.Normal # used a lot - - if isinstance(frame_info, stack_data.RepeatedFrames): return ' %s[... skipping similar frames: %s]%s\n' % ( Colors.excName, frame_info.description, ColorsNormal) indent = ' ' * INDENT_SIZE em_normal = '%s\n%s%s' % (Colors.valEm, indent, ColorsNormal) - tpl_link = '%s%%s%s' % (Colors.filenameEm, ColorsNormal) tpl_call = 'in %s%%s%s%%s%s' % (Colors.vName, Colors.valEm, ColorsNormal) tpl_call_fail = 'in %s%%s%s(***failed resolving arguments***)%s' % \ (Colors.vName, Colors.valEm, ColorsNormal) - tpl_local_var = '%s%%s%s' % (Colors.vName, ColorsNormal) tpl_name_val = '%%s %s= %%s%s' % (Colors.valEm, ColorsNormal) - file = frame_info.filename - file = py3compat.cast_unicode(file, util_path.fs_encoding) - link = tpl_link % util_path.compress_user(file) + link = _format_filename(frame_info.filename, Colors.filenameEm, ColorsNormal) args, varargs, varkw, locals_ = inspect.getargvalues(frame_info.frame) func = frame_info.executing.code_qualname() @@ -634,12 +671,20 @@ class VerboseTB(TBTools): lvals = '' lvals_list = [] if self.include_vars: - for var in frame_info.variables_in_executing_piece: - lvals_list.append(tpl_name_val % (var.name, repr(var.value))) + try: + # we likely want to fix stackdata at some point, but + # still need a workaround. + fibp = frame_info.variables_in_executing_piece + for var in fibp: + lvals_list.append(tpl_name_val % (var.name, repr(var.value))) + except Exception: + lvals_list.append( + "Exception trying to inspect frame. No more locals available." + ) if lvals_list: lvals = '%s%s' % (indent, em_normal.join(lvals_list)) - result = '%s %s\n' % (link, call) + result = "%s, %s\n" % (link, call) result += ''.join(_format_traceback_lines(frame_info.lines, Colors, self.has_colors, lvals)) return result diff --git a/IPython/extensions/autoreload.py b/IPython/extensions/autoreload.py index ada680f..f481520 100644 --- a/IPython/extensions/autoreload.py +++ b/IPython/extensions/autoreload.py @@ -48,6 +48,11 @@ The following magic commands are provided: Reload all modules (except those excluded by ``%aimport``) every time before executing the Python code typed. +``%autoreload 3`` + + Reload all modules AND autoload newly added objects + every time before executing the Python code typed. + ``%aimport`` List modules which are to be automatically imported or not to be imported. @@ -94,21 +99,21 @@ Some of the known remaining caveats are: skip_doctest = True -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Copyright (C) 2000 Thomas Heller # Copyright (C) 2008 Pauli Virtanen # Copyright (C) 2012 The IPython Development Team # # Distributed under the terms of the BSD License. The full license is in # the file COPYING, distributed as part of this software. -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # # This IPython module is written by Pauli Virtanen, based on the autoreload # code by Thomas Heller. -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Imports -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- import os import sys @@ -120,18 +125,22 @@ from importlib import import_module from importlib.util import source_from_cache from imp import reload -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ # Autoreload functionality -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -class ModuleReloader(object): + +class ModuleReloader: enabled = False """Whether this reloader is enabled""" check_all = True """Autoreload all modules, not just those listed in 'modules'""" - def __init__(self): + autoload_obj = False + """Autoreload all modules AND autoload all new objects""" + + def __init__(self, shell=None): # Modules that failed to reload: {module: mtime-on-failed-reload, ...} self.failed = {} # Modules specially marked as autoreloadable. @@ -142,6 +151,7 @@ class ModuleReloader(object): self.old_objects = {} # Module modification timestamps self.modules_mtimes = {} + self.shell = shell # Cache module modification times self.check(check_all=True, do_reload=False) @@ -176,22 +186,22 @@ class ModuleReloader(object): self.mark_module_reloadable(module_name) import_module(module_name) - top_name = module_name.split('.')[0] + top_name = module_name.split(".")[0] top_module = sys.modules[top_name] return top_module, top_name def filename_and_mtime(self, module): - if not hasattr(module, '__file__') or module.__file__ is None: + if not hasattr(module, "__file__") or module.__file__ is None: return None, None - if getattr(module, '__name__', None) in [None, '__mp_main__', '__main__']: + if getattr(module, "__name__", None) in [None, "__mp_main__", "__main__"]: # we cannot reload(__main__) or reload(__mp_main__) return None, None filename = module.__file__ path, ext = os.path.splitext(filename) - if ext.lower() == '.py': + if ext.lower() == ".py": py_filename = filename else: try: @@ -242,21 +252,35 @@ class ModuleReloader(object): # If we've reached this point, we should try to reload the module if do_reload: try: - superreload(m, reload, self.old_objects) + if self.autoload_obj: + superreload(m, reload, self.old_objects, self.shell) + else: + superreload(m, reload, self.old_objects) if py_filename in self.failed: del self.failed[py_filename] except: - print("[autoreload of %s failed: %s]" % ( - modname, traceback.format_exc(10)), file=sys.stderr) + print( + "[autoreload of {} failed: {}]".format( + modname, traceback.format_exc(10) + ), + file=sys.stderr, + ) self.failed[py_filename] = pymtime -#------------------------------------------------------------------------------ + +# ------------------------------------------------------------------------------ # superreload -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ -func_attrs = ['__code__', '__defaults__', '__doc__', - '__closure__', '__globals__', '__dict__'] +func_attrs = [ + "__code__", + "__defaults__", + "__doc__", + "__closure__", + "__globals__", + "__dict__", +] def update_function(old, new): @@ -272,7 +296,7 @@ def update_instances(old, new): """Use garbage collector to find all instances that refer to the old class definition and update their __class__ to point to the new class definition""" - + refs = gc.get_referrers(old) for ref in refs: @@ -299,19 +323,20 @@ def update_class(old, new): pass continue - if update_generic(old_obj, new_obj): continue + if update_generic(old_obj, new_obj): + continue try: setattr(old, key, getattr(new, key)) except (AttributeError, TypeError): - pass # skip non-writable attributes + pass # skip non-writable attributes for key in list(new.__dict__.keys()): if key not in list(old.__dict__.keys()): try: setattr(old, key, getattr(new, key)) except (AttributeError, TypeError): - pass # skip non-writable attributes + pass # skip non-writable attributes # update all instances of class update_instances(old, new) @@ -329,16 +354,18 @@ def isinstance2(a, b, typ): UPDATE_RULES = [ - (lambda a, b: isinstance2(a, b, type), - update_class), - (lambda a, b: isinstance2(a, b, types.FunctionType), - update_function), - (lambda a, b: isinstance2(a, b, property), - update_property), + (lambda a, b: isinstance2(a, b, type), update_class), + (lambda a, b: isinstance2(a, b, types.FunctionType), update_function), + (lambda a, b: isinstance2(a, b, property), update_property), ] -UPDATE_RULES.extend([(lambda a, b: isinstance2(a, b, types.MethodType), - lambda a, b: update_function(a.__func__, b.__func__)), -]) +UPDATE_RULES.extend( + [ + ( + lambda a, b: isinstance2(a, b, types.MethodType), + lambda a, b: update_function(a.__func__, b.__func__), + ), + ] +) def update_generic(a, b): @@ -349,14 +376,45 @@ def update_generic(a, b): return False -class StrongRef(object): +class StrongRef: def __init__(self, obj): self.obj = obj + def __call__(self): return self.obj -def superreload(module, reload=reload, old_objects=None): +mod_attrs = [ + "__name__", + "__doc__", + "__package__", + "__loader__", + "__spec__", + "__file__", + "__cached__", + "__builtins__", +] + + +def append_obj(module, d, name, obj, autoload=False): + in_module = hasattr(obj, "__module__") and obj.__module__ == module.__name__ + if autoload: + # check needed for module global built-ins + if not in_module and name in mod_attrs: + return False + else: + if not in_module: + return False + + key = (module.__name__, name) + try: + d.setdefault(key, []).append(weakref.ref(obj)) + except TypeError: + pass + return True + + +def superreload(module, reload=reload, old_objects=None, shell=None): """Enhanced version of the builtin reload function. superreload remembers objects previously in the module, and @@ -371,7 +429,7 @@ def superreload(module, reload=reload, old_objects=None): # collect old objects in the module for name, obj in list(module.__dict__.items()): - if not hasattr(obj, '__module__') or obj.__module__ != module.__name__: + if not append_obj(module, old_objects, name, obj): continue key = (module.__name__, name) try: @@ -385,8 +443,8 @@ def superreload(module, reload=reload, old_objects=None): old_dict = module.__dict__.copy() old_name = module.__name__ module.__dict__.clear() - module.__dict__['__name__'] = old_name - module.__dict__['__loader__'] = old_dict['__loader__'] + module.__dict__["__name__"] = old_name + module.__dict__["__loader__"] = old_dict["__loader__"] except (TypeError, AttributeError, KeyError): pass @@ -400,12 +458,21 @@ def superreload(module, reload=reload, old_objects=None): # iterate over all objects and update functions & classes for name, new_obj in list(module.__dict__.items()): key = (module.__name__, name) - if key not in old_objects: continue + if key not in old_objects: + # here 'shell' acts both as a flag and as an output var + if ( + shell is None + or name == "Enum" + or not append_obj(module, old_objects, name, new_obj, True) + ): + continue + shell.user_ns[name] = new_obj new_refs = [] for old_ref in old_objects[key]: old_obj = old_ref() - if old_obj is None: continue + if old_obj is None: + continue new_refs.append(old_ref) update_generic(old_obj, new_obj) @@ -416,22 +483,25 @@ def superreload(module, reload=reload, old_objects=None): return module -#------------------------------------------------------------------------------ + +# ------------------------------------------------------------------------------ # IPython connectivity -#------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ from IPython.core.magic import Magics, magics_class, line_magic + @magics_class class AutoreloadMagics(Magics): def __init__(self, *a, **kw): - super(AutoreloadMagics, self).__init__(*a, **kw) - self._reloader = ModuleReloader() + super().__init__(*a, **kw) + self._reloader = ModuleReloader(self.shell) self._reloader.check_all = False + self._reloader.autoload_obj = False self.loaded_modules = set(sys.modules) @line_magic - def autoreload(self, parameter_s=''): + def autoreload(self, parameter_s=""): r"""%autoreload => Reload modules automatically %autoreload @@ -475,19 +545,24 @@ class AutoreloadMagics(Magics): autoreloaded. """ - if parameter_s == '': + if parameter_s == "": self._reloader.check(True) - elif parameter_s == '0': + elif parameter_s == "0": self._reloader.enabled = False - elif parameter_s == '1': + elif parameter_s == "1": self._reloader.check_all = False self._reloader.enabled = True - elif parameter_s == '2': + elif parameter_s == "2": + self._reloader.check_all = True + self._reloader.enabled = True + self._reloader.enabled = True + elif parameter_s == "3": self._reloader.check_all = True self._reloader.enabled = True + self._reloader.autoload_obj = True @line_magic - def aimport(self, parameter_s='', stream=None): + def aimport(self, parameter_s="", stream=None): """%aimport => Import modules for automatic reloading. %aimport @@ -511,13 +586,13 @@ class AutoreloadMagics(Magics): if self._reloader.check_all: stream.write("Modules to reload:\nall-except-skipped\n") else: - stream.write("Modules to reload:\n%s\n" % ' '.join(to_reload)) - stream.write("\nModules to skip:\n%s\n" % ' '.join(to_skip)) - elif modname.startswith('-'): + stream.write("Modules to reload:\n%s\n" % " ".join(to_reload)) + stream.write("\nModules to skip:\n%s\n" % " ".join(to_skip)) + elif modname.startswith("-"): modname = modname[1:] self._reloader.mark_module_skipped(modname) else: - for _module in ([_.strip() for _ in modname.split(',')]): + for _module in [_.strip() for _ in modname.split(",")]: top_module, top_name = self._reloader.aimport_module(_module) # Inject module to user namespace @@ -531,8 +606,7 @@ class AutoreloadMagics(Magics): pass def post_execute_hook(self): - """Cache the modification times of any modules imported in this execution - """ + """Cache the modification times of any modules imported in this execution""" newly_loaded_modules = set(sys.modules) - self.loaded_modules for modname in newly_loaded_modules: _, pymtime = self._reloader.filename_and_mtime(sys.modules[modname]) @@ -546,5 +620,5 @@ def load_ipython_extension(ip): """Load the extension in IPython.""" auto_reload = AutoreloadMagics(ip) ip.register_magics(auto_reload) - ip.events.register('pre_run_cell', auto_reload.pre_run_cell) - ip.events.register('post_execute', auto_reload.post_execute_hook) + ip.events.register("pre_run_cell", auto_reload.pre_run_cell) + ip.events.register("post_execute", auto_reload.post_execute_hook) diff --git a/IPython/extensions/tests/test_autoreload.py b/IPython/extensions/tests/test_autoreload.py index e81bf22..a84c8c9 100644 --- a/IPython/extensions/tests/test_autoreload.py +++ b/IPython/extensions/tests/test_autoreload.py @@ -1,16 +1,16 @@ """Tests for autoreload extension. """ -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Copyright (c) 2012 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. -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Imports -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- import os import sys @@ -29,26 +29,26 @@ from unittest import TestCase from IPython.extensions.autoreload import AutoreloadMagics from IPython.core.events import EventManager, pre_run_cell -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- # Test fixture -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- noop = lambda *a, **kw: None -class FakeShell: +class FakeShell: def __init__(self): self.ns = {} self.user_ns = self.ns self.user_ns_hidden = {} - self.events = EventManager(self, {'pre_run_cell', pre_run_cell}) + self.events = EventManager(self, {"pre_run_cell", pre_run_cell}) self.auto_magics = AutoreloadMagics(shell=self) - self.events.register('pre_run_cell', self.auto_magics.pre_run_cell) + self.events.register("pre_run_cell", self.auto_magics.pre_run_cell) register_magics = set_hook = noop def run_code(self, code): - self.events.trigger('pre_run_cell') + self.events.trigger("pre_run_cell") exec(code, self.user_ns) self.auto_magics.post_execute_hook() @@ -85,7 +85,7 @@ class Fixture(TestCase): self.shell = None def get_module(self): - module_name = "tmpmod_" + "".join(random.sample(self.filename_chars,20)) + module_name = "tmpmod_" + "".join(random.sample(self.filename_chars, 20)) if module_name in sys.modules: del sys.modules[module_name] file_name = os.path.join(self.test_dir, module_name + ".py") @@ -111,19 +111,21 @@ class Fixture(TestCase): time.sleep(1.05) # Write - with open(filename, 'w') as f: + with open(filename, "w") as f: f.write(content) def new_module(self, code): code = textwrap.dedent(code) mod_name, mod_fn = self.get_module() - with open(mod_fn, 'w') as f: + with open(mod_fn, "w") as f: f.write(code) return mod_name, mod_fn -#----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- # Test automatic reloading -#----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- + def pickle_get_current_class(obj): """ @@ -136,25 +138,36 @@ def pickle_get_current_class(obj): obj2 = getattr(obj2, subpath) return obj2 -class TestAutoreload(Fixture): +class TestAutoreload(Fixture): def test_reload_enums(self): - mod_name, mod_fn = self.new_module(textwrap.dedent(""" + mod_name, mod_fn = self.new_module( + textwrap.dedent( + """ from enum import Enum class MyEnum(Enum): A = 'A' B = 'B' - """)) + """ + ) + ) self.shell.magic_autoreload("2") self.shell.magic_aimport(mod_name) - self.write_file(mod_fn, textwrap.dedent(""" + self.write_file( + mod_fn, + textwrap.dedent( + """ from enum import Enum class MyEnum(Enum): A = 'A' B = 'B' C = 'C' - """)) - with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): + """ + ), + ) + with tt.AssertNotPrints( + ("[autoreload of %s failed:" % mod_name), channel="stderr" + ): self.shell.run_code("pass") # trigger another reload def test_reload_class_type(self): @@ -195,7 +208,9 @@ class TestAutoreload(Fixture): def test_reload_class_attributes(self): self.shell.magic_autoreload("2") - mod_name, mod_fn = self.new_module(textwrap.dedent(""" + mod_name, mod_fn = self.new_module( + textwrap.dedent( + """ class MyClass: def __init__(self, a=10): @@ -241,16 +256,99 @@ class TestAutoreload(Fixture): self.shell.run_code("second = MyClass(5)") - for object_name in {'first', 'second'}: - self.shell.run_code("{object_name}.power(5)".format(object_name=object_name)) + for object_name in {"first", "second"}: + self.shell.run_code(f"{object_name}.power(5)") with nt.assert_raises(AttributeError): - self.shell.run_code("{object_name}.cube()".format(object_name=object_name)) + self.shell.run_code(f"{object_name}.cube()") with nt.assert_raises(AttributeError): - self.shell.run_code("{object_name}.square()".format(object_name=object_name)) - self.shell.run_code("{object_name}.b".format(object_name=object_name)) - self.shell.run_code("{object_name}.a".format(object_name=object_name)) + self.shell.run_code(f"{object_name}.square()") + self.shell.run_code(f"{object_name}.b") + self.shell.run_code(f"{object_name}.a") with nt.assert_raises(AttributeError): - self.shell.run_code("{object_name}.toto".format(object_name=object_name)) + self.shell.run_code(f"{object_name}.toto") + + def test_autoload_newly_added_objects(self): + self.shell.magic_autoreload("3") + mod_code = """ + def func1(): pass + """ + mod_name, mod_fn = self.new_module(textwrap.dedent(mod_code)) + self.shell.run_code(f"from {mod_name} import *") + self.shell.run_code("func1()") + with nt.assert_raises(NameError): + self.shell.run_code("func2()") + with nt.assert_raises(NameError): + self.shell.run_code("t = Test()") + with nt.assert_raises(NameError): + self.shell.run_code("number") + + # ----------- TEST NEW OBJ LOADED -------------------------- + + new_code = """ + def func1(): pass + def func2(): pass + class Test: pass + number = 0 + from enum import Enum + class TestEnum(Enum): + A = 'a' + """ + self.write_file(mod_fn, textwrap.dedent(new_code)) + + # test function now exists in shell's namespace namespace + self.shell.run_code("func2()") + # test function now exists in module's dict + self.shell.run_code(f"import sys; sys.modules['{mod_name}'].func2()") + # test class now exists + self.shell.run_code("t = Test()") + # test global built-in var now exists + self.shell.run_code("number") + # test the enumerations gets loaded succesfully + self.shell.run_code("TestEnum.A") + + # ----------- TEST NEW OBJ CAN BE CHANGED -------------------- + + new_code = """ + def func1(): return 'changed' + def func2(): return 'changed' + class Test: + def new_func(self): + return 'changed' + number = 1 + from enum import Enum + class TestEnum(Enum): + A = 'a' + B = 'added' + """ + self.write_file(mod_fn, textwrap.dedent(new_code)) + self.shell.run_code("assert func1() == 'changed'") + self.shell.run_code("assert func2() == 'changed'") + self.shell.run_code("t = Test(); assert t.new_func() == 'changed'") + self.shell.run_code("assert number == 1") + self.shell.run_code("assert TestEnum.B.value == 'added'") + + # ----------- TEST IMPORT FROM MODULE -------------------------- + + new_mod_code = """ + from enum import Enum + class Ext(Enum): + A = 'ext' + def ext_func(): + return 'ext' + class ExtTest: + def meth(self): + return 'ext' + ext_int = 2 + """ + new_mod_name, new_mod_fn = self.new_module(textwrap.dedent(new_mod_code)) + current_mod_code = f""" + from {new_mod_name} import * + """ + self.write_file(mod_fn, textwrap.dedent(current_mod_code)) + self.shell.run_code("assert Ext.A.value == 'ext'") + self.shell.run_code("assert ext_func() == 'ext'") + self.shell.run_code("t = ExtTest(); assert t.meth() == 'ext'") + self.shell.run_code("assert ext_int == 2") def _check_smoketest(self, use_aimport=True): """ @@ -258,7 +356,8 @@ class TestAutoreload(Fixture): '%autoreload 1' or '%autoreload 2' """ - mod_name, mod_fn = self.new_module(""" + mod_name, mod_fn = self.new_module( + """ x = 9 z = 123 # this item will be deleted @@ -281,7 +380,8 @@ class Baz(object): class Bar: # old-style class: weakref doesn't work for it on Python < 2.7 def foo(self): return 1 -""") +""" + ) # # Import module, and mark for reloading @@ -300,8 +400,9 @@ class Bar: # old-style class: weakref doesn't work for it on Python < 2.7 self.shell.run_code("import %s" % mod_name) stream = StringIO() self.shell.magic_aimport("", stream=stream) - nt.assert_true("Modules to reload:\nall-except-skipped" in - stream.getvalue()) + nt.assert_true( + "Modules to reload:\nall-except-skipped" in stream.getvalue() + ) nt.assert_in(mod_name, self.shell.ns) mod = sys.modules[mod_name] @@ -336,20 +437,29 @@ class Bar: # old-style class: weakref doesn't work for it on Python < 2.7 # Simulate a failed reload: no reload should occur and exactly # one error message should be printed # - self.write_file(mod_fn, """ + self.write_file( + mod_fn, + """ a syntax error -""") +""", + ) - with tt.AssertPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): - self.shell.run_code("pass") # trigger reload - with tt.AssertNotPrints(('[autoreload of %s failed:' % mod_name), channel='stderr'): - self.shell.run_code("pass") # trigger another reload + with tt.AssertPrints( + ("[autoreload of %s failed:" % mod_name), channel="stderr" + ): + self.shell.run_code("pass") # trigger reload + with tt.AssertNotPrints( + ("[autoreload of %s failed:" % mod_name), channel="stderr" + ): + self.shell.run_code("pass") # trigger another reload check_module_contents() # # Rewrite module (this time reload should succeed) # - self.write_file(mod_fn, """ + self.write_file( + mod_fn, + """ x = 10 def foo(y): @@ -367,30 +477,31 @@ class Baz(object): class Bar: # old-style class def foo(self): return 2 -""") +""", + ) def check_module_contents(): nt.assert_equal(mod.x, 10) - nt.assert_false(hasattr(mod, 'z')) + nt.assert_false(hasattr(mod, "z")) - nt.assert_equal(old_foo(0), 4) # superreload magic! + nt.assert_equal(old_foo(0), 4) # superreload magic! nt.assert_equal(mod.foo(0), 4) obj = mod.Baz(9) - nt.assert_equal(old_obj.bar(1), 11) # superreload magic! + nt.assert_equal(old_obj.bar(1), 11) # superreload magic! nt.assert_equal(obj.bar(1), 11) nt.assert_equal(old_obj.quux, 43) nt.assert_equal(obj.quux, 43) - nt.assert_false(hasattr(old_obj, 'zzz')) - nt.assert_false(hasattr(obj, 'zzz')) + nt.assert_false(hasattr(old_obj, "zzz")) + nt.assert_false(hasattr(obj, "zzz")) obj2 = mod.Bar() nt.assert_equal(old_obj2.foo(), 2) nt.assert_equal(obj2.foo(), 2) - self.shell.run_code("pass") # trigger reload + self.shell.run_code("pass") # trigger reload check_module_contents() # @@ -398,7 +509,7 @@ class Bar: # old-style class # os.unlink(mod_fn) - self.shell.run_code("pass") # trigger reload + self.shell.run_code("pass") # trigger reload check_module_contents() # @@ -408,19 +519,21 @@ class Bar: # old-style class self.shell.magic_aimport("-" + mod_name) stream = StringIO() self.shell.magic_aimport("", stream=stream) - nt.assert_true(("Modules to skip:\n%s" % mod_name) in - stream.getvalue()) + nt.assert_true(("Modules to skip:\n%s" % mod_name) in stream.getvalue()) # This should succeed, although no such module exists self.shell.magic_aimport("-tmpmod_as318989e89ds") else: self.shell.magic_autoreload("0") - self.write_file(mod_fn, """ + self.write_file( + mod_fn, + """ x = -99 -""") +""", + ) - self.shell.run_code("pass") # trigger reload + self.shell.run_code("pass") # trigger reload self.shell.run_code("pass") check_module_contents() @@ -432,7 +545,7 @@ x = -99 else: self.shell.magic_autoreload("") - self.shell.run_code("pass") # trigger reload + self.shell.run_code("pass") # trigger reload nt.assert_equal(mod.x, -99) def test_smoketest_aimport(self): @@ -440,8 +553,3 @@ x = -99 def test_smoketest_autoreload(self): self._check_smoketest(use_aimport=False) - - - - - diff --git a/IPython/external/qt_for_kernel.py b/IPython/external/qt_for_kernel.py index 1a94e7e..d2e7bd9 100644 --- a/IPython/external/qt_for_kernel.py +++ b/IPython/external/qt_for_kernel.py @@ -32,15 +32,42 @@ import os import sys from IPython.utils.version import check_version -from IPython.external.qt_loaders import (load_qt, loaded_api, QT_API_PYSIDE, - QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, - QT_API_PYQTv1, QT_API_PYQT_DEFAULT) +from IPython.external.qt_loaders import ( + load_qt, + loaded_api, + enum_factory, + # QT6 + QT_API_PYQT6, + QT_API_PYSIDE6, + # QT5 + QT_API_PYQT5, + QT_API_PYSIDE2, + # QT4 + QT_API_PYQTv1, + QT_API_PYQT, + QT_API_PYSIDE, + # default + QT_API_PYQT_DEFAULT, +) + +_qt_apis = ( + # QT6 + QT_API_PYQT6, + QT_API_PYSIDE6, + # QT5 + QT_API_PYQT5, + QT_API_PYSIDE2, + # QT4 + QT_API_PYQTv1, + QT_API_PYQT, + QT_API_PYSIDE, + # default + QT_API_PYQT_DEFAULT, +) -_qt_apis = (QT_API_PYSIDE, QT_API_PYSIDE2, QT_API_PYQT, QT_API_PYQT5, QT_API_PYQTv1, - QT_API_PYQT_DEFAULT) -#Constraints placed on an imported matplotlib def matplotlib_options(mpl): + """Constraints placed on an imported matplotlib.""" if mpl is None: return backend = mpl.rcParams.get('backend', None) @@ -66,9 +93,7 @@ def matplotlib_options(mpl): mpqt) def get_options(): - """Return a list of acceptable QT APIs, in decreasing order of - preference - """ + """Return a list of acceptable QT APIs, in decreasing order of preference.""" #already imported Qt somewhere. Use that loaded = loaded_api() if loaded is not None: @@ -83,13 +108,22 @@ def get_options(): qt_api = os.environ.get('QT_API', None) if qt_api is None: #no ETS variable. Ask mpl, then use default fallback path - return matplotlib_options(mpl) or [QT_API_PYQT_DEFAULT, QT_API_PYSIDE, - QT_API_PYQT5, QT_API_PYSIDE2] + return matplotlib_options(mpl) or [ + QT_API_PYQT_DEFAULT, + QT_API_PYQT6, + QT_API_PYSIDE6, + QT_API_PYQT5, + QT_API_PYSIDE2, + QT_API_PYQT, + QT_API_PYSIDE, + ] elif qt_api not in _qt_apis: raise RuntimeError("Invalid Qt API %r, valid values are: %r" % (qt_api, ', '.join(_qt_apis))) else: return [qt_api] + api_opts = get_options() QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts) +enum_helper = enum_factory(QT_API, QtCore) diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index ca7483e..7980535 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -10,26 +10,41 @@ be accessed directly from the outside """ import sys import types -from functools import partial -from importlib import import_module +from functools import partial, lru_cache +import operator from IPython.utils.version import check_version -# Available APIs. -QT_API_PYQT = 'pyqt' # Force version 2 +# ### Available APIs. +# Qt6 +QT_API_PYQT6 = "pyqt6" +QT_API_PYSIDE6 = "pyside6" + +# Qt5 QT_API_PYQT5 = 'pyqt5' -QT_API_PYQTv1 = 'pyqtv1' # Force version 2 -QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2 -QT_API_PYSIDE = 'pyside' QT_API_PYSIDE2 = 'pyside2' -api_to_module = {QT_API_PYSIDE2: 'PySide2', - QT_API_PYSIDE: 'PySide', - QT_API_PYQT: 'PyQt4', - QT_API_PYQTv1: 'PyQt4', - QT_API_PYQT5: 'PyQt5', - QT_API_PYQT_DEFAULT: 'PyQt4', - } +# Qt4 +QT_API_PYQT = "pyqt" # Force version 2 +QT_API_PYQTv1 = "pyqtv1" # Force version 2 +QT_API_PYSIDE = "pyside" + +QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2 + +api_to_module = { + # Qt6 + QT_API_PYQT6: "PyQt6", + QT_API_PYSIDE6: "PySide6", + # Qt5 + QT_API_PYQT5: "PyQt5", + QT_API_PYSIDE2: "PySide2", + # Qt4 + QT_API_PYSIDE: "PySide", + QT_API_PYQT: "PyQt4", + QT_API_PYQTv1: "PyQt4", + # default + QT_API_PYQT_DEFAULT: "PyQt6", +} class ImportDenier(object): @@ -56,6 +71,7 @@ class ImportDenier(object): already imported an Incompatible QT Binding: %s """ % (fullname, loaded_api())) + ID = ImportDenier() sys.meta_path.insert(0, ID) @@ -63,23 +79,11 @@ sys.meta_path.insert(0, ID) def commit_api(api): """Commit to a particular API, and trigger ImportErrors on subsequent dangerous imports""" + modules = set(api_to_module.values()) - if api == QT_API_PYSIDE2: - ID.forbid('PySide') - ID.forbid('PyQt4') - ID.forbid('PyQt5') - elif api == QT_API_PYSIDE: - ID.forbid('PySide2') - ID.forbid('PyQt4') - ID.forbid('PyQt5') - elif api == QT_API_PYQT5: - ID.forbid('PySide2') - ID.forbid('PySide') - ID.forbid('PyQt4') - else: # There are three other possibilities, all representing PyQt4 - ID.forbid('PyQt5') - ID.forbid('PySide2') - ID.forbid('PySide') + modules.remove(api_to_module[api]) + for mod in modules: + ID.forbid(mod) def loaded_api(): @@ -90,19 +94,24 @@ def loaded_api(): Returns ------- - None, 'pyside2', 'pyside', 'pyqt', 'pyqt5', or 'pyqtv1' + None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1' """ - if 'PyQt4.QtCore' in sys.modules: + if sys.modules.get("PyQt6.QtCore"): + return QT_API_PYQT6 + elif sys.modules.get("PySide6.QtCore"): + return QT_API_PYSIDE6 + elif sys.modules.get("PyQt5.QtCore"): + return QT_API_PYQT5 + elif sys.modules.get("PySide2.QtCore"): + return QT_API_PYSIDE2 + elif sys.modules.get("PyQt4.QtCore"): if qtapi_version() == 2: return QT_API_PYQT else: return QT_API_PYQTv1 - elif 'PySide.QtCore' in sys.modules: + elif sys.modules.get("PySide.QtCore"): return QT_API_PYSIDE - elif 'PySide2.QtCore' in sys.modules: - return QT_API_PYSIDE2 - elif 'PyQt5.QtCore' in sys.modules: - return QT_API_PYQT5 + return None @@ -122,7 +131,7 @@ def has_binding(api): from importlib.util import find_spec required = ['QtCore', 'QtGui', 'QtSvg'] - if api in (QT_API_PYQT5, QT_API_PYSIDE2): + if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6): # QT5 requires QtWidgets too required.append('QtWidgets') @@ -174,7 +183,7 @@ def can_import(api): current = loaded_api() if api == QT_API_PYQT_DEFAULT: - return current in [QT_API_PYQT, QT_API_PYQTv1, None] + return current in [QT_API_PYQT6, None] else: return current in [api, None] @@ -224,7 +233,7 @@ def import_pyqt5(): """ from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui - + # Alias PyQt-specific functions for PySide compatibility. QtCore.Signal = QtCore.pyqtSignal QtCore.Slot = QtCore.pyqtSlot @@ -238,6 +247,28 @@ def import_pyqt5(): return QtCore, QtGuiCompat, QtSvg, api +def import_pyqt6(): + """ + Import PyQt6 + + ImportErrors rasied within this function are non-recoverable + """ + + from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui + + # Alias PyQt-specific functions for PySide compatibility. + QtCore.Signal = QtCore.pyqtSignal + QtCore.Slot = QtCore.pyqtSlot + + # Join QtGui and QtWidgets for Qt4 compatibility. + QtGuiCompat = types.ModuleType("QtGuiCompat") + QtGuiCompat.__dict__.update(QtGui.__dict__) + QtGuiCompat.__dict__.update(QtWidgets.__dict__) + + api = QT_API_PYQT6 + return QtCore, QtGuiCompat, QtSvg, api + + def import_pyside(): """ Import PySide @@ -264,6 +295,23 @@ def import_pyside2(): return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2 +def import_pyside6(): + """ + Import PySide6 + + ImportErrors raised within this function are non-recoverable + """ + from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport + + # Join QtGui and QtWidgets for Qt4 compatibility. + QtGuiCompat = types.ModuleType("QtGuiCompat") + QtGuiCompat.__dict__.update(QtGui.__dict__) + QtGuiCompat.__dict__.update(QtWidgets.__dict__) + QtGuiCompat.__dict__.update(QtPrintSupport.__dict__) + + return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6 + + def load_qt(api_options): """ Attempt to import Qt, given a preference list @@ -291,13 +339,19 @@ def load_qt(api_options): an incompatible library has already been installed) """ loaders = { - QT_API_PYSIDE2: import_pyside2, - QT_API_PYSIDE: import_pyside, - QT_API_PYQT: import_pyqt4, - QT_API_PYQT5: import_pyqt5, - QT_API_PYQTv1: partial(import_pyqt4, version=1), - QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None) - } + # Qt6 + QT_API_PYQT6: import_pyqt6, + QT_API_PYSIDE6: import_pyside6, + # Qt5 + QT_API_PYQT5: import_pyqt5, + QT_API_PYSIDE2: import_pyside2, + # Qt4 + QT_API_PYSIDE: import_pyside, + QT_API_PYQT: import_pyqt4, + QT_API_PYQTv1: partial(import_pyqt4, version=1), + # default + QT_API_PYQT_DEFAULT: import_pyqt6, + } for api in api_options: @@ -332,3 +386,16 @@ def load_qt(api_options): has_binding(QT_API_PYSIDE), has_binding(QT_API_PYSIDE2), api_options)) + + +def enum_factory(QT_API, QtCore): + """Construct an enum helper to account for PyQt5 <-> PyQt6 changes.""" + + @lru_cache(None) + def _enum(name): + # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6). + return operator.attrgetter( + name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0] + )(sys.modules[QtCore.__package__]) + + return _enum diff --git a/IPython/html.py b/IPython/html.py deleted file mode 100644 index 050be5c..0000000 --- a/IPython/html.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -Shim to maintain backwards compatibility with old IPython.html imports. -""" -# Copyright (c) IPython Development Team. -# Distributed under the terms of the Modified BSD License. - -import sys -from warnings import warn - -from IPython.utils.shimmodule import ShimModule, ShimWarning - -warn("The `IPython.html` package has been deprecated since IPython 4.0. " - "You should import from `notebook` instead. " - "`IPython.html.widgets` has moved to `ipywidgets`.", ShimWarning) - -_widgets = sys.modules['IPython.html.widgets'] = ShimModule( - src='IPython.html.widgets', mirror='ipywidgets') - -_html = ShimModule( - src='IPython.html', mirror='notebook') - -# hook up widgets -_html.widgets = _widgets -sys.modules['IPython.html'] = _html - -if __name__ == '__main__': - from notebook import notebookapp as app - app.launch_new_instance() diff --git a/IPython/lib/display.py b/IPython/lib/display.py index e8b8a44..19d4c35 100644 --- a/IPython/lib/display.py +++ b/IPython/lib/display.py @@ -8,6 +8,8 @@ from os import walk, sep, fsdecode from IPython.core.display import DisplayObject, TextDisplayObject +from typing import Tuple, Iterable + __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument', 'FileLink', 'FileLinks', 'Code'] @@ -159,7 +161,7 @@ class Audio(DisplayObject): return val @staticmethod - def _validate_and_normalize_with_numpy(data, normalize): + def _validate_and_normalize_with_numpy(data, normalize) -> Tuple[bytes, int]: import numpy as np data = np.array(data, dtype=float) @@ -178,8 +180,7 @@ class Audio(DisplayObject): max_abs_value = np.max(np.abs(data)) normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize) scaled = data / normalization_factor * 32767 - return scaled.astype(' """ - def __init__(self, src, width, height, **kwargs): + def __init__(self, src, width, height, extras: Iterable[str] = None, **kwargs): + if extras is None: + extras = [] + self.src = src self.width = width self.height = height + self.extras = extras self.params = kwargs def _repr_html_(self): @@ -278,10 +284,14 @@ class IFrame(object): params = "?" + urlencode(self.params) else: params = "" - return self.iframe.format(src=self.src, - width=self.width, - height=self.height, - params=params) + return self.iframe.format( + src=self.src, + width=self.width, + height=self.height, + params=params, + extras=" ".join(self.extras), + ) + class YouTubeVideo(IFrame): """Class for embedding a YouTube Video in an IPython session, based on its video id. @@ -309,11 +319,14 @@ class YouTubeVideo(IFrame): will be inserted in the document. """ - def __init__(self, id, width=400, height=300, **kwargs): + def __init__(self, id, width=400, height=300, allow_autoplay=False, **kwargs): self.id=id src = "https://www.youtube.com/embed/{0}".format(id) + if allow_autoplay: + extras = list(kwargs.get("extras", [])) + ['allow="autoplay"'] + kwargs.update(autoplay=1, extras=extras) super(YouTubeVideo, self).__init__(src, width, height, **kwargs) - + def _repr_jpeg_(self): # Deferred import from urllib.request import urlopen diff --git a/IPython/lib/editorhooks.py b/IPython/lib/editorhooks.py index 7ce0577..d8bd6ac 100644 --- a/IPython/lib/editorhooks.py +++ b/IPython/lib/editorhooks.py @@ -6,7 +6,6 @@ Contributions are *very* welcome. """ import os -import pipes import shlex import subprocess import sys @@ -47,9 +46,9 @@ def install_editor(template, wait=False): def call_editor(self, filename, line=0): if line is None: line = 0 - cmd = template.format(filename=pipes.quote(filename), line=line) + cmd = template.format(filename=shlex.quote(filename), line=line) print(">", cmd) - # pipes.quote doesn't work right on Windows, but it does after splitting + # shlex.quote doesn't work right on Windows, but it does after splitting if sys.platform.startswith('win'): cmd = shlex.split(cmd) proc = subprocess.Popen(cmd, shell=True) diff --git a/IPython/lib/inputhookgtk4.py b/IPython/lib/inputhookgtk4.py new file mode 100644 index 0000000..a872cee --- /dev/null +++ b/IPython/lib/inputhookgtk4.py @@ -0,0 +1,43 @@ +""" +Enable Gtk4 to be used interactively by IPython. +""" +# ----------------------------------------------------------------------------- +# Copyright (c) 2021, the 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. +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- + +import sys + +from gi.repository import GLib + +# ----------------------------------------------------------------------------- +# Code +# ----------------------------------------------------------------------------- + + +class _InputHook: + def __init__(self, context): + self._quit = False + GLib.io_add_watch(sys.stdin, GLib.PRIORITY_DEFAULT, GLib.IO_IN, self.quit) + + def quit(self, *args, **kwargs): + self._quit = True + return False + + def run(self): + context = GLib.MainContext.default() + while not self._quit: + context.iteration(True) + + +def inputhook_gtk4(): + hook = _InputHook() + hook.run() + return 0 diff --git a/IPython/lib/latextools.py b/IPython/lib/latextools.py index 920b045..540d3f5 100644 --- a/IPython/lib/latextools.py +++ b/IPython/lib/latextools.py @@ -111,7 +111,8 @@ def latex_to_png(s, encode=False, backend=None, wrap=False, color='Black', def latex_to_png_mpl(s, wrap, color='Black', scale=1.0): try: - from matplotlib import mathtext + from matplotlib import figure, font_manager, mathtext + from matplotlib.backends import backend_agg from pyparsing import ParseFatalException except ImportError: return None @@ -122,11 +123,18 @@ def latex_to_png_mpl(s, wrap, color='Black', scale=1.0): s = u'${0}$'.format(s) try: - mt = mathtext.MathTextParser('bitmap') - f = BytesIO() - dpi = 120*scale - mt.to_png(f, s, fontsize=12, dpi=dpi, color=color) - return f.getvalue() + prop = font_manager.FontProperties(size=12) + dpi = 120 * scale + buffer = BytesIO() + + # Adapted from mathtext.math_to_image + parser = mathtext.MathTextParser("path") + width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop) + fig = figure.Figure(figsize=(width / 72, height / 72)) + fig.text(0, depth / height, s, fontproperties=prop, color=color) + backend_agg.FigureCanvasAgg(fig) + fig.savefig(buffer, dpi=dpi, format="png", transparent=True) + return buffer.getvalue() except (ValueError, RuntimeError, ParseFatalException): return None diff --git a/IPython/lib/pretty.py b/IPython/lib/pretty.py index 921c19d..7a18f01 100644 --- a/IPython/lib/pretty.py +++ b/IPython/lib/pretty.py @@ -626,7 +626,7 @@ def _default_pprint(obj, p, cycle): def _seq_pprinter_factory(start, end): """ Factory that returns a pprint function useful for sequences. Used by - the default pprint for tuples, dicts, and lists. + the default pprint for tuples and lists. """ def inner(obj, p, cycle): if cycle: diff --git a/IPython/lib/tests/test_backgroundjobs.py b/IPython/lib/tests/test_backgroundjobs.py index d7793f5..fc76ff1 100644 --- a/IPython/lib/tests/test_backgroundjobs.py +++ b/IPython/lib/tests/test_backgroundjobs.py @@ -15,9 +15,6 @@ # Stdlib imports import time -# Third-party imports -import nose.tools as nt - # Our own imports from IPython.lib import backgroundjobs as bg @@ -49,18 +46,18 @@ def test_result(): jobs = bg.BackgroundJobManager() j = jobs.new(sleeper) j.join() - nt.assert_equal(j.result['interval'], t_short) - + assert j.result["interval"] == t_short + def test_flush(): """Test job control""" jobs = bg.BackgroundJobManager() j = jobs.new(sleeper) j.join() - nt.assert_equal(len(jobs.completed), 1) - nt.assert_equal(len(jobs.dead), 0) + assert len(jobs.completed) == 1 + assert len(jobs.dead) == 0 jobs.flush() - nt.assert_equal(len(jobs.completed), 0) + assert len(jobs.completed) == 0 def test_dead(): @@ -68,10 +65,10 @@ def test_dead(): jobs = bg.BackgroundJobManager() j = jobs.new(crasher) j.join() - nt.assert_equal(len(jobs.completed), 0) - nt.assert_equal(len(jobs.dead), 1) + assert len(jobs.completed) == 0 + assert len(jobs.dead) == 1 jobs.flush() - nt.assert_equal(len(jobs.dead), 0) + assert len(jobs.dead) == 0 def test_longer(): @@ -81,8 +78,8 @@ def test_longer(): # job as running, but not so long that it makes the test suite noticeably # slower. j = jobs.new(sleeper, 0.1) - nt.assert_equal(len(jobs.running), 1) - nt.assert_equal(len(jobs.completed), 0) + assert len(jobs.running) == 1 + assert len(jobs.completed) == 0 j.join() - nt.assert_equal(len(jobs.running), 0) - nt.assert_equal(len(jobs.completed), 1) + assert len(jobs.running) == 0 + assert len(jobs.completed) == 1 diff --git a/IPython/lib/tests/test_display.py b/IPython/lib/tests/test_display.py index 7e98a18..414d3fd 100644 --- a/IPython/lib/tests/test_display.py +++ b/IPython/lib/tests/test_display.py @@ -59,8 +59,9 @@ def test_existing_path_FileLink(): tf = NamedTemporaryFile() fl = display.FileLink(tf.name) actual = fl._repr_html_() - expected = "%s
" % (tf.name,tf.name) - nt.assert_equal(actual,expected) + expected = "%s
" % (tf.name, tf.name) + assert actual == expected + def test_existing_path_FileLink_repr(): """FileLink: Calling repr() functions as expected on existing filepath @@ -69,7 +70,8 @@ def test_existing_path_FileLink_repr(): fl = display.FileLink(tf.name) actual = repr(fl) expected = tf.name - nt.assert_equal(actual,expected) + assert actual == expected + def test_error_on_directory_to_FileLink(): """FileLink: Raises error when passed directory @@ -111,7 +113,8 @@ def test_existing_path_FileLinks(): (tf1.name.replace("\\","/"),split(tf1.name)[1])] expected.sort() # We compare the sorted list of links here as that's more reliable - nt.assert_equal(actual,expected) + assert actual == expected + def test_existing_path_FileLinks_alt_formatter(): """FileLinks: Calling _repr_html_ functions as expected w/ an alt formatter @@ -128,7 +131,8 @@ def test_existing_path_FileLinks_alt_formatter(): expected = ["hello","world"] expected.sort() # We compare the sorted list of links here as that's more reliable - nt.assert_equal(actual,expected) + assert actual == expected + def test_existing_path_FileLinks_repr(): """FileLinks: Calling repr() functions as expected on existing directory """ @@ -142,8 +146,9 @@ def test_existing_path_FileLinks_repr(): expected = ['%s/' % td, ' %s' % split(tf1.name)[1],' %s' % split(tf2.name)[1]] expected.sort() # We compare the sorted list of links here as that's more reliable - nt.assert_equal(actual,expected) - + assert actual == expected + + def test_existing_path_FileLinks_repr_alt_formatter(): """FileLinks: Calling repr() functions as expected w/ alt formatter """ @@ -159,8 +164,9 @@ def test_existing_path_FileLinks_repr_alt_formatter(): expected = ["hello","world"] expected.sort() # We compare the sorted list of links here as that's more reliable - nt.assert_equal(actual,expected) - + assert actual == expected + + def test_error_on_file_to_FileLinks(): """FileLinks: Raises error when passed file """ @@ -178,11 +184,11 @@ def test_recursive_FileLinks(): fl = display.FileLinks(td) actual = str(fl) actual = actual.split('\n') - nt.assert_equal(len(actual), 4, actual) + assert len(actual) == 4, actual fl = display.FileLinks(td, recursive=False) actual = str(fl) actual = actual.split('\n') - nt.assert_equal(len(actual), 2, actual) + assert len(actual) == 2, actual def test_audio_from_file(): path = pjoin(dirname(__file__), 'test.wav') @@ -194,13 +200,13 @@ class TestAudioDataWithNumpy(TestCase): def test_audio_from_numpy_array(self): test_tone = get_test_tone() audio = display.Audio(test_tone, rate=44100) - nt.assert_equal(len(read_wav(audio.data)), len(test_tone)) + assert len(read_wav(audio.data)) == len(test_tone) @skipif_not_numpy def test_audio_from_list(self): test_tone = get_test_tone() audio = display.Audio(list(test_tone), rate=44100) - nt.assert_equal(len(read_wav(audio.data)), len(test_tone)) + assert len(read_wav(audio.data)) == len(test_tone) @skipif_not_numpy def test_audio_from_numpy_array_without_rate_raises(self): @@ -212,7 +218,7 @@ class TestAudioDataWithNumpy(TestCase): for scale in [1, 0.5, 2]: audio = display.Audio(get_test_tone(scale), rate=44100) actual_max_value = numpy.max(numpy.abs(read_wav(audio.data))) - nt.assert_equal(actual_max_value, expected_max_value) + assert actual_max_value == expected_max_value @skipif_not_numpy def test_audio_data_without_normalization(self): @@ -223,7 +229,7 @@ class TestAudioDataWithNumpy(TestCase): expected_max_value = int(max_int16 * test_tone_max_abs) audio = display.Audio(test_tone, rate=44100, normalize=False) actual_max_value = numpy.max(numpy.abs(read_wav(audio.data))) - nt.assert_equal(actual_max_value, expected_max_value) + assert actual_max_value == expected_max_value def test_audio_data_without_normalization_raises_for_invalid_data(self): nt.assert_raises( diff --git a/IPython/lib/tests/test_editorhooks.py b/IPython/lib/tests/test_editorhooks.py index 658276c..6e33547 100644 --- a/IPython/lib/tests/test_editorhooks.py +++ b/IPython/lib/tests/test_editorhooks.py @@ -2,8 +2,6 @@ import sys from unittest import mock -import nose.tools as nt - from IPython import get_ipython from IPython.lib import editorhooks @@ -20,15 +18,15 @@ def test_install_editor(): with mock.patch('subprocess.Popen', fake_popen): get_ipython().hooks.editor('the file', 64) - nt.assert_equal(len(called), 1) - args = called[0]['args'] - kwargs = called[0]['kwargs'] - - nt.assert_equal(kwargs, {'shell': True}) - - if sys.platform.startswith('win'): - expected = ['foo', '-l', '64', '-f', 'the file'] + assert len(called) == 1 + args = called[0]["args"] + kwargs = called[0]["kwargs"] + + assert kwargs == {"shell": True} + + if sys.platform.startswith("win"): + expected = ["foo", "-l", "64", "-f", "the file"] else: expected = "foo -l 64 -f 'the file'" cmd = args[0] - nt.assert_equal(cmd, expected) + assert cmd == expected diff --git a/IPython/lib/tests/test_latextools.py b/IPython/lib/tests/test_latextools.py index 794688c..fafff73 100644 --- a/IPython/lib/tests/test_latextools.py +++ b/IPython/lib/tests/test_latextools.py @@ -25,7 +25,7 @@ def test_check_latex_to_png_dvipng_fails_when_no_cmd(command): raise FindCmdError with patch.object(latextools, "find_cmd", mock_find_cmd): - assert latextools.latex_to_png_dvipng("whatever", True) == None + assert latextools.latex_to_png_dvipng("whatever", True) is None @contextmanager diff --git a/IPython/lib/tests/test_pretty.py b/IPython/lib/tests/test_pretty.py index 36cc5d2..3c8cbf2 100644 --- a/IPython/lib/tests/test_pretty.py +++ b/IPython/lib/tests/test_pretty.py @@ -81,7 +81,7 @@ def test_indentation(): gotoutput = pretty.pretty(MyList(range(count))) expectedoutput = "MyList(\n" + ",\n".join(" %d" % i for i in range(count)) + ")" - nt.assert_equal(gotoutput, expectedoutput) + assert gotoutput == expectedoutput def test_dispatch(): @@ -92,7 +92,7 @@ def test_dispatch(): gotoutput = pretty.pretty(MyDict()) expectedoutput = "MyDict(...)" - nt.assert_equal(gotoutput, expectedoutput) + assert gotoutput == expectedoutput def test_callability_checking(): @@ -103,7 +103,7 @@ def test_callability_checking(): gotoutput = pretty.pretty(Dummy2()) expectedoutput = "Dummy1(...)" - nt.assert_equal(gotoutput, expectedoutput) + assert gotoutput == expectedoutput @pytest.mark.parametrize( @@ -135,7 +135,7 @@ def test_sets(obj, expected_output): Test that set and frozenset use Python 3 formatting. """ got_output = pretty.pretty(obj) - nt.assert_equal(got_output, expected_output) + assert got_output == expected_output @skip_without('xxlimited') @@ -145,22 +145,24 @@ def test_pprint_heap_allocated_type(): """ import xxlimited output = pretty.pretty(xxlimited.Null) - nt.assert_equal(output, 'xxlimited.Null') + assert output == "xxlimited.Null" + def test_pprint_nomod(): """ Test that pprint works for classes with no __module__. """ output = pretty.pretty(NoModule) - nt.assert_equal(output, 'NoModule') - + assert output == "NoModule" + + def test_pprint_break(): """ Test that p.break_ produces expected output """ output = pretty.pretty(Breaking()) expected = "TG: Breaking(\n ):" - nt.assert_equal(output, expected) + assert output == expected def test_pprint_break_repr(): """ @@ -168,11 +170,11 @@ def test_pprint_break_repr(): """ output = pretty.pretty([[BreakingRepr()]]) expected = "[[Breaking(\n )]]" - nt.assert_equal(output, expected) + assert output == expected output = pretty.pretty([[BreakingRepr()]*2]) expected = "[[Breaking(\n ),\n Breaking(\n )]]" - nt.assert_equal(output, expected) + assert output == expected def test_bad_repr(): """Don't catch bad repr errors""" @@ -258,7 +260,7 @@ ClassWithMeta = MetaClass('ClassWithMeta') def test_metaclass_repr(): output = pretty.pretty(ClassWithMeta) - nt.assert_equal(output, "[CUSTOM REPR FOR CLASS ClassWithMeta]") + assert output == "[CUSTOM REPR FOR CLASS ClassWithMeta]" def test_unicode_repr(): @@ -271,9 +273,9 @@ def test_unicode_repr(): c = C() p = pretty.pretty(c) - nt.assert_equal(p, u) + assert p == u p = pretty.pretty([c]) - nt.assert_equal(p, u'[%s]' % u) + assert p == u"[%s]" % u def test_basic_class(): @@ -290,10 +292,11 @@ def test_basic_class(): printer.flush() output = stream.getvalue() - nt.assert_equal(output, '%s.MyObj' % __name__) + assert output == "%s.MyObj" % __name__ nt.assert_true(type_pprint_wrapper.called) +# TODO : pytest.mark.parametrise once nose is gone. def test_collections_defaultdict(): # Create defaultdicts with cycles a = defaultdict() @@ -311,9 +314,10 @@ def test_collections_defaultdict(): (b, "defaultdict(list, {'key': defaultdict(...)})"), ] for obj, expected in cases: - nt.assert_equal(pretty.pretty(obj), expected) + assert pretty.pretty(obj) == expected +# TODO : pytest.mark.parametrise once nose is gone. def test_collections_ordereddict(): # Create OrderedDict with cycle a = OrderedDict() @@ -335,9 +339,10 @@ def test_collections_ordereddict(): (a, "OrderedDict([('key', OrderedDict(...))])"), ] for obj, expected in cases: - nt.assert_equal(pretty.pretty(obj), expected) + assert pretty.pretty(obj) == expected +# TODO : pytest.mark.parametrise once nose is gone. def test_collections_deque(): # Create deque with cycle a = deque() @@ -369,8 +374,10 @@ def test_collections_deque(): (a, 'deque([deque(...)])'), ] for obj, expected in cases: - nt.assert_equal(pretty.pretty(obj), expected) + assert pretty.pretty(obj) == expected + +# TODO : pytest.mark.parametrise once nose is gone. def test_collections_counter(): class MyCounter(Counter): pass @@ -380,8 +387,9 @@ def test_collections_counter(): (MyCounter(a=1), "MyCounter({'a': 1})"), ] for obj, expected in cases: - nt.assert_equal(pretty.pretty(obj), expected) + assert pretty.pretty(obj) == expected +# TODO : pytest.mark.parametrise once nose is gone. def test_mappingproxy(): MP = types.MappingProxyType underlying_dict = {} @@ -424,9 +432,10 @@ def test_mappingproxy(): "{2: mappingproxy({2: {...}, 3: {...}}), 3: {...}}"), ] for obj, expected in cases: - nt.assert_equal(pretty.pretty(obj), expected) + assert pretty.pretty(obj) == expected +# TODO : pytest.mark.parametrise once nose is gone. def test_simplenamespace(): SN = types.SimpleNamespace @@ -444,7 +453,7 @@ def test_simplenamespace(): (sn_recursive, "namespace(first=namespace(...), second=namespace(...))"), ] for obj, expected in cases: - nt.assert_equal(pretty.pretty(obj), expected) + assert pretty.pretty(obj) == expected def test_pretty_environ(): @@ -452,7 +461,7 @@ def test_pretty_environ(): # reindent to align with 'environ' prefix dict_indented = dict_repr.replace('\n', '\n' + (' ' * len('environ'))) env_repr = pretty.pretty(os.environ) - nt.assert_equal(env_repr, 'environ' + dict_indented) + assert env_repr == "environ" + dict_indented def test_function_pretty(): @@ -460,8 +469,9 @@ def test_function_pretty(): # posixpath is a pure python module, its interface is consistent # across Python distributions import posixpath - nt.assert_equal(pretty.pretty(posixpath.join), '') - + + assert pretty.pretty(posixpath.join) == "" + # custom function def meaning_of_life(question=None): if question: @@ -489,4 +499,4 @@ def test_custom_repr(): oc = OrderedCounter("abracadabra") nt.assert_in("OrderedCounter(OrderedDict", pretty.pretty(oc)) - nt.assert_equal(pretty.pretty(MySet()), 'mine') + assert pretty.pretty(MySet()) == "mine" diff --git a/IPython/lib/tests/test_security.py b/IPython/lib/tests/test_security.py index 7d89ba1..27c32ab 100644 --- a/IPython/lib/tests/test_security.py +++ b/IPython/lib/tests/test_security.py @@ -1,26 +1,27 @@ # coding: utf-8 from IPython.lib import passwd from IPython.lib.security import passwd_check, salt_len -import nose.tools as nt def test_passwd_structure(): - p = passwd('passphrase') - algorithm, salt, hashed = p.split(':') - nt.assert_equal(algorithm, 'sha1') - nt.assert_equal(len(salt), salt_len) - nt.assert_equal(len(hashed), 40) + p = passwd("passphrase") + algorithm, salt, hashed = p.split(":") + assert algorithm == "sha1" + assert len(salt) == salt_len + assert len(hashed) == 40 def test_roundtrip(): - p = passwd('passphrase') - nt.assert_equal(passwd_check(p, 'passphrase'), True) + p = passwd("passphrase") + assert passwd_check(p, "passphrase") is True + def test_bad(): p = passwd('passphrase') - nt.assert_equal(passwd_check(p, p), False) - nt.assert_equal(passwd_check(p, 'a:b:c:d'), False) - nt.assert_equal(passwd_check(p, 'a:b'), False) + assert passwd_check(p, p) is False + assert passwd_check(p, "a:b:c:d") is False + assert passwd_check(p, "a:b") is False + def test_passwd_check_unicode(): # GH issue #4524 phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f' - assert passwd_check(phash, u"łe¶ŧ←↓→") \ No newline at end of file + assert passwd_check(phash, u"łe¶ŧ←↓→") diff --git a/IPython/sphinxext/ipython_directive.py b/IPython/sphinxext/ipython_directive.py index 3d5b376..ac09640 100644 --- a/IPython/sphinxext/ipython_directive.py +++ b/IPython/sphinxext/ipython_directive.py @@ -200,6 +200,7 @@ from io import StringIO # Third-party from docutils.parsers.rst import directives from docutils.parsers.rst import Directive +from sphinx.util import logging # Our own from traitlets.config import Config @@ -557,15 +558,20 @@ class EmbeddedSphinxShell(object): filename = self.directive.state.document.current_source lineno = self.directive.state.document.current_line + # Use sphinx logger for warnings + logger = logging.getLogger(__name__) + # output any exceptions raised during execution to stdout # unless :okexcept: has been specified. - if not is_okexcept and (("Traceback" in processed_output) or ("SyntaxError" in processed_output)): - s = "\nException in %s at block ending on line %s\n" % (filename, lineno) + if not is_okexcept and ( + ("Traceback" in processed_output) or ("SyntaxError" in processed_output) + ): + s = "\n>>>" + ("-" * 73) + "\n" + s += "Exception in %s at block ending on line %s\n" % (filename, lineno) s += "Specify :okexcept: as an option in the ipython:: block to suppress this message\n" - sys.stdout.write('\n\n>>>' + ('-' * 73)) - sys.stdout.write(s) - sys.stdout.write(processed_output) - sys.stdout.write('<<<' + ('-' * 73) + '\n\n') + s += processed_output + "\n" + s += "<<<" + ("-" * 73) + logger.warning(s) if self.warning_is_error: raise RuntimeError('Non Expected exception in `{}` line {}'.format(filename, lineno)) @@ -573,15 +579,15 @@ class EmbeddedSphinxShell(object): # unless :okwarning: has been specified. if not is_okwarning: for w in ws: - s = "\nWarning in %s at block ending on line %s\n" % (filename, lineno) + s = "\n>>>" + ("-" * 73) + "\n" + s += "Warning in %s at block ending on line %s\n" % (filename, lineno) s += "Specify :okwarning: as an option in the ipython:: block to suppress this message\n" - sys.stdout.write('\n\n>>>' + ('-' * 73)) - sys.stdout.write(s) - sys.stdout.write(('-' * 76) + '\n') - s=warnings.formatwarning(w.message, w.category, - w.filename, w.lineno, w.line) - sys.stdout.write(s) - sys.stdout.write('<<<' + ('-' * 73) + '\n') + s += ("-" * 76) + "\n" + s += warnings.formatwarning( + w.message, w.category, w.filename, w.lineno, w.line + ) + s += "<<<" + ("-" * 73) + logger.warning(s) if self.warning_is_error: raise RuntimeError('Non Expected warning in `{}` line {}'.format(filename, lineno)) @@ -1002,6 +1008,9 @@ class IPythonDirective(Directive): lines = ['.. code-block:: ipython', ''] figures = [] + # Use sphinx logger for warnings + logger = logging.getLogger(__name__) + for part in parts: block = block_parser(part, rgxin, rgxout, promptin, promptout) if len(block): @@ -1020,7 +1029,7 @@ class IPythonDirective(Directive): if self.shell.warning_is_error: raise RuntimeError(message) else: - warnings.warn(message) + logger.warning(message) for figure in figures: lines.append('') diff --git a/IPython/terminal/debugger.py b/IPython/terminal/debugger.py index dd1ecc1..57aa5dd 100644 --- a/IPython/terminal/debugger.py +++ b/IPython/terminal/debugger.py @@ -7,6 +7,7 @@ from IPython.core.debugger import Pdb from IPython.core.completer import IPCompleter from .ptutils import IPythonPTCompleter from .shortcuts import create_ipython_shortcuts +from . import embed from pygments.token import Token from prompt_toolkit.shortcuts.prompt import PromptSession @@ -131,6 +132,18 @@ class TerminalPdb(Pdb): except Exception: raise + def do_interact(self, arg): + ipshell = embed.InteractiveShellEmbed( + config=self.shell.config, + banner1="*interactive*", + exit_msg="*exiting interactive console...*", + ) + global_ns = self.curframe.f_globals + ipshell( + module=sys.modules.get(global_ns["__name__"], None), + local_ns=self.curframe_locals, + ) + def set_trace(frame=None): """ @@ -148,6 +161,6 @@ if __name__ == '__main__': # happened after hitting "c", this is needed in order to # be able to quit the debugging session (see #9950). old_trace_dispatch = pdb.Pdb.trace_dispatch - pdb.Pdb = TerminalPdb - pdb.Pdb.trace_dispatch = old_trace_dispatch + pdb.Pdb = TerminalPdb # type: ignore + pdb.Pdb.trace_dispatch = old_trace_dispatch # type: ignore pdb.main() diff --git a/IPython/terminal/embed.py b/IPython/terminal/embed.py index 188844f..d2cbe88 100644 --- a/IPython/terminal/embed.py +++ b/IPython/terminal/embed.py @@ -19,6 +19,8 @@ from IPython.terminal.ipapp import load_default_config from traitlets import Bool, CBool, Unicode from IPython.utils.io import ask_yes_no +from typing import Set + class KillEmbedded(Exception):pass # kept for backward compatibility as IPython 6 was released with @@ -123,17 +125,17 @@ class InteractiveShellEmbed(TerminalInteractiveShell): help="Automatically set the terminal title" ).tag(config=True) - _inactive_locations = set() + _inactive_locations: Set[str] = set() + + def _disable_init_location(self): + """Disable the current Instance creation location""" + InteractiveShellEmbed._inactive_locations.add(self._init_location_id) @property def embedded_active(self): return (self._call_location_id not in InteractiveShellEmbed._inactive_locations)\ and (self._init_location_id not in InteractiveShellEmbed._inactive_locations) - def _disable_init_location(self): - """Disable the current Instance creation location""" - InteractiveShellEmbed._inactive_locations.add(self._init_location_id) - @embedded_active.setter def embedded_active(self, value): if value: @@ -334,7 +336,7 @@ class InteractiveShellEmbed(TerminalInteractiveShell): self.compile.flags = orig_compile_flags -def embed(**kwargs): +def embed(*, header="", compile_flags=None, **kwargs): """Call this to embed IPython at the current point in your program. The first invocation of this will create an :class:`InteractiveShellEmbed` @@ -360,8 +362,6 @@ def embed(**kwargs): config argument. """ config = kwargs.get('config') - header = kwargs.pop('header', u'') - compile_flags = kwargs.pop('compile_flags', None) if config is None: config = load_default_config() config.InteractiveShellEmbed = config.TerminalInteractiveShell diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index 324246a..2714412 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -204,9 +204,8 @@ class TerminalInteractiveShell(InteractiveShell): @observe('editing_mode') def _editing_mode(self, change): - u_mode = change.new.upper() if self.pt_app: - self.pt_app.editing_mode = u_mode + self.pt_app.editing_mode = getattr(EditingMode, change.new.upper()) @observe('autoformatter') def _autoformatter_changed(self, change): @@ -615,6 +614,13 @@ class TerminalInteractiveShell(InteractiveShell): self.restore_term_title() + # try to call some at-exit operation optimistically as some things can't + # be done during interpreter shutdown. this is technically inaccurate as + # this make mainlool not re-callable, but that should be a rare if not + # in existent use case. + + self._atexit_once() + _inputhook = None def inputhook(self, context): diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index c7ba58d..69ff0ba 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -7,13 +7,20 @@ aliases = { } backends = [ - 'qt', 'qt4', 'qt5', - 'gtk', 'gtk2', 'gtk3', - 'tk', - 'wx', - 'pyglet', 'glut', - 'osx', - 'asyncio' + "qt", + "qt4", + "qt5", + "qt6", + "gtk", + "gtk2", + "gtk3", + "gtk4", + "tk", + "wx", + "pyglet", + "glut", + "osx", + "asyncio", ] registered = {} @@ -22,6 +29,7 @@ def register(name, inputhook): """Register the function *inputhook* as an event loop integration.""" registered[name] = inputhook + class UnknownBackend(KeyError): def __init__(self, name): self.name = name @@ -31,6 +39,7 @@ class UnknownBackend(KeyError): "Supported event loops are: {}").format(self.name, ', '.join(backends + sorted(registered))) + def get_inputhook_name_and_func(gui): if gui in registered: return gui, registered[gui] @@ -42,9 +51,12 @@ def get_inputhook_name_and_func(gui): return get_inputhook_name_and_func(aliases[gui]) gui_mod = gui - if gui == 'qt5': - os.environ['QT_API'] = 'pyqt5' - gui_mod = 'qt' + if gui == "qt5": + os.environ["QT_API"] = "pyqt5" + gui_mod = "qt" + elif gui == "qt6": + os.environ["QT_API"] = "pyqt6" + gui_mod = "qt" mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod) return gui, mod.inputhook diff --git a/IPython/terminal/pt_inputhooks/gtk4.py b/IPython/terminal/pt_inputhooks/gtk4.py new file mode 100644 index 0000000..009fbf1 --- /dev/null +++ b/IPython/terminal/pt_inputhooks/gtk4.py @@ -0,0 +1,27 @@ +""" +prompt_toolkit input hook for GTK 4. +""" + +from gi.repository import GLib + + +class _InputHook: + def __init__(self, context): + self._quit = False + GLib.io_add_watch( + context.fileno(), GLib.PRIORITY_DEFAULT, GLib.IO_IN, self.quit + ) + + def quit(self, *args, **kwargs): + self._quit = True + return False + + def run(self): + context = GLib.MainContext.default() + while not self._quit: + context.iteration(True) + + +def inputhook(context): + hook = _InputHook(context) + hook.run() diff --git a/IPython/terminal/pt_inputhooks/osx.py b/IPython/terminal/pt_inputhooks/osx.py index 8044019..2754820 100644 --- a/IPython/terminal/pt_inputhooks/osx.py +++ b/IPython/terminal/pt_inputhooks/osx.py @@ -9,7 +9,7 @@ import ctypes import ctypes.util from threading import Event -objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) +objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("objc")) # type: ignore void_p = ctypes.c_void_p @@ -37,7 +37,7 @@ def C(classname): # end obj-c boilerplate from appnope # CoreFoundation C-API calls we will use: -CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library('CoreFoundation')) +CoreFoundation = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation")) # type: ignore CFFileDescriptorCreate = CoreFoundation.CFFileDescriptorCreate CFFileDescriptorCreate.restype = void_p diff --git a/IPython/terminal/pt_inputhooks/qt.py b/IPython/terminal/pt_inputhooks/qt.py index b7683b8..bc34f70 100644 --- a/IPython/terminal/pt_inputhooks/qt.py +++ b/IPython/terminal/pt_inputhooks/qt.py @@ -1,6 +1,6 @@ import sys import os -from IPython.external.qt_for_kernel import QtCore, QtGui +from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper from IPython import get_ipython # If we create a QApplication, keep a reference to it so that it doesn't get @@ -9,6 +9,11 @@ _appref = None _already_warned = False +def _exec(obj): + # exec on PyQt6, exec_ elsewhere. + obj.exec() if hasattr(obj, "exec") else obj.exec_() + + def _reclaim_excepthook(): shell = get_ipython() if shell is not None: @@ -32,7 +37,16 @@ def inputhook(context): 'variable. Deactivate Qt5 code.' ) return - QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) + try: + QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) + except AttributeError: # Only for Qt>=5.6, <6. + pass + try: + QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy( + QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough + ) + except AttributeError: # Only for Qt>=5.14. + pass _appref = app = QtGui.QApplication([" "]) # "reclaim" IPython sys.excepthook after event loop starts @@ -55,8 +69,9 @@ def inputhook(context): else: # On POSIX platforms, we can use a file descriptor to quit the event # loop when there is input ready to read. - notifier = QtCore.QSocketNotifier(context.fileno(), - QtCore.QSocketNotifier.Read) + notifier = QtCore.QSocketNotifier( + context.fileno(), enum_helper("QtCore.QSocketNotifier.Type").Read + ) try: # connect the callback we care about before we turn it on # lambda is necessary as PyQT inspect the function signature to know @@ -65,6 +80,6 @@ def inputhook(context): notifier.setEnabled(True) # only start the event loop we are not already flipped if not context.input_is_ready(): - event_loop.exec_() + _exec(event_loop) finally: notifier.setEnabled(False) diff --git a/IPython/utils/_process_common.py b/IPython/utils/_process_common.py index 2a647dc..2a0b828 100644 --- a/IPython/utils/_process_common.py +++ b/IPython/utils/_process_common.py @@ -49,18 +49,16 @@ def process_handler(cmd, callback, stderr=subprocess.PIPE): Parameters ---------- cmd : str or list - A command to be executed by the system, using :class:`subprocess.Popen`. - If a string is passed, it will be run in the system shell. If a list is - passed, it will be used directly as arguments. - + A command to be executed by the system, using :class:`subprocess.Popen`. + If a string is passed, it will be run in the system shell. If a list is + passed, it will be used directly as arguments. callback : callable - A one-argument function that will be called with the Popen object. - + A one-argument function that will be called with the Popen object. stderr : file descriptor number, optional - By default this is set to ``subprocess.PIPE``, but you can also pass the - value ``subprocess.STDOUT`` to force the subprocess' stderr to go into - the same file descriptor as its stdout. This is useful to read stdout - and stderr combined in the order they are generated. + By default this is set to ``subprocess.PIPE``, but you can also pass the + value ``subprocess.STDOUT`` to force the subprocess' stderr to go into + the same file descriptor as its stdout. This is useful to read stdout + and stderr combined in the order they are generated. Returns ------- @@ -117,12 +115,12 @@ def getoutput(cmd): Parameters ---------- cmd : str or list - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- output : str - A string containing the combination of stdout and stderr from the + A string containing the combination of stdout and stderr from the subprocess, in whatever order the subprocess originally wrote to its file descriptors (so the order of the information in this string is the correct order as would be seen if running the command in a terminal). @@ -141,7 +139,7 @@ def getoutputerror(cmd): Parameters ---------- cmd : str or list - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- @@ -159,7 +157,7 @@ def get_output_error_code(cmd): Parameters ---------- cmd : str or list - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- diff --git a/IPython/utils/_process_posix.py b/IPython/utils/_process_posix.py index a11cad7..a9b0e21 100644 --- a/IPython/utils/_process_posix.py +++ b/IPython/utils/_process_posix.py @@ -82,12 +82,12 @@ class ProcessHandler(object): Parameters ---------- cmd : str - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- output : str - A string containing the combination of stdout and stderr from the + A string containing the combination of stdout and stderr from the subprocess, in whatever order the subprocess originally wrote to its file descriptors (so the order of the information in this string is the correct order as would be seen if running the command in a terminal). @@ -103,12 +103,12 @@ class ProcessHandler(object): Parameters ---------- cmd : str - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- output : str - A string containing the combination of stdout and stderr from the + A string containing the combination of stdout and stderr from the subprocess, in whatever order the subprocess originally wrote to its file descriptors (so the order of the information in this string is the correct order as would be seen if running the command in a terminal). @@ -124,7 +124,7 @@ class ProcessHandler(object): Parameters ---------- cmd : str - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- diff --git a/IPython/utils/_process_win32.py b/IPython/utils/_process_win32.py index 86d8100..2f072a8 100644 --- a/IPython/utils/_process_win32.py +++ b/IPython/utils/_process_win32.py @@ -128,7 +128,7 @@ def system(cmd): Parameters ---------- cmd : str or list - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- @@ -152,7 +152,7 @@ def getoutput(cmd): Parameters ---------- cmd : str or list - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- @@ -181,7 +181,7 @@ try: This is a special version for windows that use a ctypes call to CommandLineToArgvW to do the argv splitting. The posix parameter is ignored. - + If strict=False, process_common.arg_split(...strict=False) is used instead. """ #CommandLineToArgvW returns path to executable if called with empty string. diff --git a/IPython/utils/_process_win32_controller.py b/IPython/utils/_process_win32_controller.py index c2e2329..f8c2a05 100644 --- a/IPython/utils/_process_win32_controller.py +++ b/IPython/utils/_process_win32_controller.py @@ -551,13 +551,13 @@ def system(cmd): Parameters ---------- cmd : str - A command to be executed in the system shell. + A command to be executed in the system shell. Returns ------- None : we explicitly do NOT return the subprocess status code, as this utility is meant to be used extensively in IPython, where any return value - would trigger :func:`sys.displayhook` calls. + would trigger : func:`sys.displayhook` calls. """ with AvoidUNCPath() as path: if path is not None: diff --git a/IPython/utils/decorators.py b/IPython/utils/decorators.py index c264855..47791d7 100644 --- a/IPython/utils/decorators.py +++ b/IPython/utils/decorators.py @@ -50,7 +50,7 @@ def flag_calls(func): def undoc(func): """Mark a function or class as undocumented. - + This is found by inspecting the AST, so for now it must be used directly as @undoc, not as e.g. @decorators.undoc """ diff --git a/IPython/utils/encoding.py b/IPython/utils/encoding.py index 69a319e..651ee0c 100644 --- a/IPython/utils/encoding.py +++ b/IPython/utils/encoding.py @@ -37,10 +37,10 @@ def get_stream_enc(stream, default=None): # won't need to make changes all over IPython. def getdefaultencoding(prefer_stream=True): """Return IPython's guess for the default encoding for bytes as text. - + If prefer_stream is True (default), asks for stdin.encoding first, to match the calling Terminal, but that is often None for subprocesses. - + Then fall back on locale.getpreferredencoding(), which should be a sensible platform default (that respects LANG environment), and finally to sys.getdefaultencoding() which is the most conservative option, diff --git a/IPython/utils/frame.py b/IPython/utils/frame.py index 74c6d41..808906b 100644 --- a/IPython/utils/frame.py +++ b/IPython/utils/frame.py @@ -28,12 +28,10 @@ def extract_vars(*names,**kw): *names : str One or more variable names which will be extracted from the caller's frame. - - depth : integer, optional + **kw : integer, optional How many frames in the stack to walk when looking for your variables. The default is 0, which will use the frame where the call was made. - Examples -------- :: diff --git a/IPython/utils/generics.py b/IPython/utils/generics.py index fcada6f..3626ca4 100644 --- a/IPython/utils/generics.py +++ b/IPython/utils/generics.py @@ -22,7 +22,6 @@ def complete_object(obj, prev_completions): The object to complete. prev_completions : list List of attributes discovered so far. - This should return the list of attributes in obj. If you only wish to add to the attributes already discovered normally, return own_attrs + prev_completions. diff --git a/IPython/utils/importstring.py b/IPython/utils/importstring.py index c7a9cce..51bfc7b 100644 --- a/IPython/utils/importstring.py +++ b/IPython/utils/importstring.py @@ -16,12 +16,12 @@ def import_item(name): Parameters ---------- name : string - The fully qualified name of the module/package being imported. + The fully qualified name of the module/package being imported. Returns ------- mod : module object - The module that was imported. + The module that was imported. """ parts = name.rsplit('.', 1) diff --git a/IPython/utils/io.py b/IPython/utils/io.py index 7dc8986..1600fc3 100644 --- a/IPython/utils/io.py +++ b/IPython/utils/io.py @@ -113,11 +113,9 @@ class Tee(object): Parameters ---------- file_or_name : filename or open filehandle (writable) - File that will be duplicated - + File that will be duplicated mode : optional, valid mode for open(). - If a filename was give, open with this mode. - + If a filename was give, open with this mode. channel : str, one of ['stdout', 'stderr'] """ if channel not in ['stdout', 'stderr']: @@ -195,15 +193,14 @@ def temp_pyfile(src, ext='.py'): Parameters ---------- src : string or list of strings (no need for ending newlines if list) - Source code to be written to the file. - + Source code to be written to the file. ext : optional, string - Extension for the generated file. + Extension for the generated file. Returns ------- (filename, open filehandle) - It is the caller's responsibility to close the open file and unlink it. + It is the caller's responsibility to close the open file and unlink it. """ fname = tempfile.mkstemp(ext)[1] with open(Path(fname), "w") as f: diff --git a/IPython/utils/ipstruct.py b/IPython/utils/ipstruct.py index 376fbbe..d71a75e 100644 --- a/IPython/utils/ipstruct.py +++ b/IPython/utils/ipstruct.py @@ -43,14 +43,13 @@ class Struct(dict): Parameters ---------- - args : dict, Struct + *args : dict, Struct Initialize with one dict or Struct - kw : dict + **kw : dict Initialize with key, value pairs. Examples -------- - >>> s = Struct(a=10,b=30) >>> s.a 10 @@ -68,7 +67,6 @@ class Struct(dict): Examples -------- - >>> s = Struct() >>> s['a'] = 10 >>> s.allow_new_attr(False) @@ -95,7 +93,6 @@ class Struct(dict): Examples -------- - >>> s = Struct() >>> s.a = 10 >>> s.a @@ -130,7 +127,6 @@ class Struct(dict): Examples -------- - >>> s = Struct(a=10) >>> s.a 10 @@ -155,7 +151,6 @@ class Struct(dict): Examples -------- - >>> s = Struct(a=10,b=30) >>> s2 = Struct(a=20,c=40) >>> s += s2 @@ -170,7 +165,6 @@ class Struct(dict): Examples -------- - >>> s1 = Struct(a=10,b=30) >>> s2 = Struct(a=20,c=40) >>> s = s1 + s2 @@ -186,7 +180,6 @@ class Struct(dict): Examples -------- - >>> s1 = Struct(a=10,b=30) >>> s2 = Struct(a=40) >>> s = s1 - s2 @@ -202,7 +195,6 @@ class Struct(dict): Examples -------- - >>> s1 = Struct(a=10,b=30) >>> s2 = Struct(a=40) >>> s1 -= s2 @@ -236,7 +228,6 @@ class Struct(dict): Examples -------- - >>> s = Struct(a=10,b=30) >>> s2 = s.copy() >>> type(s2) is Struct @@ -251,7 +242,6 @@ class Struct(dict): Examples -------- - >>> s = Struct(a=10) >>> s.hasattr('a') True @@ -284,7 +274,7 @@ class Struct(dict): Parameters ---------- - __loc_data : dict, Struct + __loc_data__ : dict, Struct The data to merge into self __conflict_solve : dict The conflict policy dict. The keys are binary functions used to @@ -292,12 +282,11 @@ class Struct(dict): the keys the conflict resolution function applies to. Instead of a list of strings a space separated string can be used, like 'a b c'. - kw : dict + **kw : dict Additional key, value pairs to merge in Notes ----- - The `__conflict_solve` dict is a dictionary of binary functions which will be used to solve key conflicts. Here is an example:: @@ -338,7 +327,6 @@ class Struct(dict): Examples -------- - This show the default policy: >>> s = Struct(a=10,b=30) diff --git a/IPython/utils/module_paths.py b/IPython/utils/module_paths.py index 32ecab8..b5ae60c 100644 --- a/IPython/utils/module_paths.py +++ b/IPython/utils/module_paths.py @@ -43,7 +43,7 @@ def find_mod(module_name): """ Find module `module_name` on sys.path, and return the path to module `module_name`. - - If `module_name` refers to a module directory, then return path to __init__ file. + - If `module_name` refers to a module directory, then return path to __init__ file. - If `module_name` is a directory without an __init__file, return None. - If module is missing or does not have a `.py` or `.pyw` extension, return None. - Note that we are not interested in running bytecode. @@ -52,7 +52,7 @@ def find_mod(module_name): Parameters ---------- module_name : str - + Returns ------- module_path : str diff --git a/IPython/utils/openpy.py b/IPython/utils/openpy.py index aca2cf0..297a762 100644 --- a/IPython/utils/openpy.py +++ b/IPython/utils/openpy.py @@ -60,15 +60,15 @@ def strip_encoding_cookie(filelike): def read_py_file(filename, skip_encoding_cookie=True): """Read a Python file, using the encoding declared inside the file. - + Parameters ---------- filename : str - The path to the file to read. + The path to the file to read. skip_encoding_cookie : bool - If True (the default), and the encoding declaration is found in the first - two lines, that line will be excluded from the output. - + If True (the default), and the encoding declaration is found in the first + two lines, that line will be excluded from the output. + Returns ------- A unicode string containing the contents of the file. @@ -82,18 +82,18 @@ def read_py_file(filename, skip_encoding_cookie=True): def read_py_url(url, errors='replace', skip_encoding_cookie=True): """Read a Python file from a URL, using the encoding declared inside the file. - + Parameters ---------- url : str - The URL from which to fetch the file. + The URL from which to fetch the file. errors : str - How to handle decoding errors in the file. Options are the same as for - bytes.decode(), but here 'replace' is the default. + How to handle decoding errors in the file. Options are the same as for + bytes.decode(), but here 'replace' is the default. skip_encoding_cookie : bool - If True (the default), and the encoding declaration is found in the first - two lines, that line will be excluded from the output. - + If True (the default), and the encoding declaration is found in the first + two lines, that line will be excluded from the output. + Returns ------- A unicode string containing the contents of the file. diff --git a/IPython/utils/path.py b/IPython/utils/path.py index 3f0e217..cc10dff 100644 --- a/IPython/utils/path.py +++ b/IPython/utils/path.py @@ -109,7 +109,7 @@ def get_py_filename(name, force_win32=None): raise IOError('File `%r` not found.' % name) -def filefind(filename, path_dirs=None): +def filefind(filename: str, path_dirs=None) -> str: """Find a file by looking through a sequence of paths. This iterates through a sequence of paths looking for a file and returns @@ -139,7 +139,12 @@ def filefind(filename, path_dirs=None): Returns ------- - Raises :exc:`IOError` or returns absolute path to file. + path : str + returns absolute path to file. + + Raises + ------ + IOError """ # If paths are quoted, abspath gets confused, strip them... @@ -178,7 +183,6 @@ def get_home_dir(require_writable=False) -> str: Parameters ---------- - require_writable : bool [default: False] if True: guarantees the return value is a writable directory, otherwise diff --git a/IPython/utils/pickleutil.py b/IPython/utils/pickleutil.py deleted file mode 100644 index 785e6f6..0000000 --- a/IPython/utils/pickleutil.py +++ /dev/null @@ -1,5 +0,0 @@ -from warnings import warn - -warn("IPython.utils.pickleutil has moved to ipykernel.pickleutil", stacklevel=2) - -from ipykernel.pickleutil import * diff --git a/IPython/utils/sysinfo.py b/IPython/utils/sysinfo.py index 07d14fd..fed82a2 100644 --- a/IPython/utils/sysinfo.py +++ b/IPython/utils/sysinfo.py @@ -40,15 +40,15 @@ def pkg_commit_hash(pkg_path): Parameters ---------- pkg_path : str - directory containing package - only used for getting commit from active repo + directory containing package + only used for getting commit from active repo Returns ------- hash_from : str - Where we got the hash from - description + Where we got the hash from - description hash_str : str - short form of hash + short form of hash """ # Try and get commit from written commit text file if _sysinfo.commit: @@ -71,12 +71,12 @@ def pkg_info(pkg_path): Parameters ---------- pkg_path : str - path containing __init__.py for package + path containing __init__.py for package Returns ------- context : dict - with named parameters of interest + with named parameters of interest """ src, hsh = pkg_commit_hash(pkg_path) return dict( diff --git a/IPython/utils/terminal.py b/IPython/utils/terminal.py index 4e18002..49fd3fe 100644 --- a/IPython/utils/terminal.py +++ b/IPython/utils/terminal.py @@ -45,7 +45,7 @@ def toggle_set_term_title(val): Parameters ---------- - val : bool + val : bool If True, set_term_title() actually writes to the terminal (using the appropriate platform-specific module). If False, it is a no-op. """ diff --git a/IPython/utils/tests/test_capture.py b/IPython/utils/tests/test_capture.py index 60b63d3..3cfa827 100644 --- a/IPython/utils/tests/test_capture.py +++ b/IPython/utils/tests/test_capture.py @@ -15,7 +15,6 @@ import sys -import nose.tools as nt import pytest from IPython.testing.decorators import skip_iptest_but_not_pytest @@ -75,18 +74,18 @@ def test_rich_output_empty(method_mime): """RichOutput with no args""" rich = capture.RichOutput() method, mime = method_mime - nt.assert_equal(getattr(rich, method)(), None) + assert getattr(rich, method)() is None def test_rich_output(): """test RichOutput basics""" data = basic_data metadata = basic_metadata rich = capture.RichOutput(data=data, metadata=metadata) - nt.assert_equal(rich._repr_html_(), data["text/html"]) - nt.assert_equal(rich._repr_png_(), (data["image/png"], metadata["image/png"])) - nt.assert_equal(rich._repr_latex_(), None) - nt.assert_equal(rich._repr_javascript_(), None) - nt.assert_equal(rich._repr_svg_(), None) + assert rich._repr_html_() == data["text/html"] + assert rich._repr_png_() == (data["image/png"], metadata["image/png"]) + assert rich._repr_latex_() is None + assert rich._repr_javascript_() is None + assert rich._repr_svg_() is None @skip_iptest_but_not_pytest @@ -96,7 +95,7 @@ def test_rich_output_no_metadata(method_mime): data = full_data rich = capture.RichOutput(data=data) method, mime = method_mime - nt.assert_equal(getattr(rich, method)(), data[mime]) + assert getattr(rich, method)() == data[mime] @skip_iptest_but_not_pytest @@ -107,11 +106,11 @@ def test_rich_output_metadata(method_mime): metadata = full_metadata rich = capture.RichOutput(data=data, metadata=metadata) method, mime = method_mime - nt.assert_equal(getattr(rich, method)(), (data[mime], metadata[mime])) + assert getattr(rich, method)() == (data[mime], metadata[mime]) def test_rich_output_display(): """test RichOutput.display - + This is a bit circular, because we are actually using the capture code we are testing to test itself. """ @@ -119,10 +118,10 @@ def test_rich_output_display(): rich = capture.RichOutput(data=data) with capture.capture_output() as cap: rich.display() - nt.assert_equal(len(cap.outputs), 1) + assert len(cap.outputs) == 1 rich2 = cap.outputs[0] - nt.assert_equal(rich2.data, rich.data) - nt.assert_equal(rich2.metadata, rich.metadata) + assert rich2.data == rich.data + assert rich2.metadata == rich.metadata def test_capture_output(): """capture_output works""" @@ -131,8 +130,8 @@ def test_capture_output(): print(hello_stdout, end="") print(hello_stderr, end="", file=sys.stderr) rich.display() - nt.assert_equal(hello_stdout, cap.stdout) - nt.assert_equal(hello_stderr, cap.stderr) + assert hello_stdout == cap.stdout + assert hello_stderr == cap.stderr def test_capture_output_no_stdout(): @@ -142,9 +141,9 @@ def test_capture_output_no_stdout(): print(hello_stdout, end="") print(hello_stderr, end="", file=sys.stderr) rich.display() - nt.assert_equal("", cap.stdout) - nt.assert_equal(hello_stderr, cap.stderr) - nt.assert_equal(len(cap.outputs), 1) + assert "" == cap.stdout + assert hello_stderr == cap.stderr + assert len(cap.outputs) == 1 def test_capture_output_no_stderr(): @@ -155,9 +154,9 @@ def test_capture_output_no_stderr(): print(hello_stdout, end="") print(hello_stderr, end="", file=sys.stderr) rich.display() - nt.assert_equal(hello_stdout, cap.stdout) - nt.assert_equal("", cap.stderr) - nt.assert_equal(len(cap.outputs), 1) + assert hello_stdout == cap.stdout + assert "" == cap.stderr + assert len(cap.outputs) == 1 def test_capture_output_no_display(): @@ -167,6 +166,6 @@ def test_capture_output_no_display(): print(hello_stdout, end="") print(hello_stderr, end="", file=sys.stderr) rich.display() - nt.assert_equal(hello_stdout, cap.stdout) - nt.assert_equal(hello_stderr, cap.stderr) - nt.assert_equal(cap.outputs, []) + assert hello_stdout == cap.stdout + assert hello_stderr == cap.stderr + assert cap.outputs == [] diff --git a/IPython/utils/tests/test_module_paths.py b/IPython/utils/tests/test_module_paths.py index 01694a2..ef2841b 100644 --- a/IPython/utils/tests/test_module_paths.py +++ b/IPython/utils/tests/test_module_paths.py @@ -54,8 +54,8 @@ def setup_module(): def teardown_module(): """Teardown testenvironment for the module: - - Remove tempdir - - restore sys.path + - Remove tempdir + - restore sys.path """ # Note: we remove the parent test dir, which is the root of all test # subdirs we may have created. Use shutil instead of os.removedirs, so diff --git a/IPython/utils/tests/test_path.py b/IPython/utils/tests/test_path.py index 74f21c3..a7be2be 100644 --- a/IPython/utils/tests/test_path.py +++ b/IPython/utils/tests/test_path.py @@ -56,7 +56,7 @@ HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir") def setup_module(): """Setup testenvironment for the module: - - Adds dummy home dir tree + - Adds dummy home dir tree """ # Do not mask exceptions here. In particular, catching WindowsError is a # problem because that exception is only defined on Windows... @@ -66,7 +66,7 @@ def setup_module(): def teardown_module(): """Teardown testenvironment for the module: - - Remove dummy home dir tree + - Remove dummy home dir tree """ # Note: we remove the parent test dir, which is the root of all test # subdirs we may have created. Use shutil instead of os.removedirs, so @@ -113,7 +113,7 @@ def test_get_home_dir_1(): IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py")) home_dir = path.get_home_dir() - nt.assert_equal(home_dir, unfrozen) + assert home_dir == unfrozen @skip_if_not_win32 @@ -127,7 +127,7 @@ def test_get_home_dir_2(): IPython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py")).lower() home_dir = path.get_home_dir(True) - nt.assert_equal(home_dir, unfrozen) + assert home_dir == unfrozen @skip_win32_py38 @@ -137,7 +137,7 @@ def test_get_home_dir_3(): env["HOME"] = HOME_TEST_DIR home_dir = path.get_home_dir(True) # get_home_dir expands symlinks - nt.assert_equal(home_dir, os.path.realpath(env["HOME"])) + assert home_dir == os.path.realpath(env["HOME"]) @with_environment @@ -181,7 +181,7 @@ def test_get_home_dir_8(): with patch.object(wreg, 'OpenKey', return_value=key()), \ patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]): home_dir = path.get_home_dir() - nt.assert_equal(home_dir, abspath(HOME_TEST_DIR)) + assert home_dir == abspath(HOME_TEST_DIR) @with_environment def test_get_xdg_dir_0(): @@ -195,7 +195,7 @@ def test_get_xdg_dir_0(): env.pop('IPYTHONDIR', None) env.pop('XDG_CONFIG_HOME', None) - nt.assert_equal(path.get_xdg_dir(), os.path.join('somewhere', '.config')) + assert path.get_xdg_dir() == os.path.join("somewhere", ".config") @with_environment @@ -208,7 +208,7 @@ def test_get_xdg_dir_1(): env.pop('IPYTHON_DIR', None) env.pop('IPYTHONDIR', None) env.pop('XDG_CONFIG_HOME', None) - nt.assert_equal(path.get_xdg_dir(), None) + assert path.get_xdg_dir() is None @with_environment def test_get_xdg_dir_2(): @@ -224,7 +224,7 @@ def test_get_xdg_dir_2(): if not os.path.exists(cfgdir): os.makedirs(cfgdir) - nt.assert_equal(path.get_xdg_dir(), cfgdir) + assert path.get_xdg_dir() == cfgdir @with_environment def test_get_xdg_dir_3(): @@ -240,7 +240,7 @@ def test_get_xdg_dir_3(): if not os.path.exists(cfgdir): os.makedirs(cfgdir) - nt.assert_equal(path.get_xdg_dir(), None) + assert path.get_xdg_dir() is None def test_filefind(): """Various tests for filefind""" @@ -263,13 +263,13 @@ def test_get_long_path_name_win32(): # Test to see if the short path evaluates correctly. short_path = os.path.join(tmpdir, 'THISIS~1') evaluated_path = path.get_long_path_name(short_path) - nt.assert_equal(evaluated_path.lower(), long_path.lower()) + assert evaluated_path.lower() == long_path.lower() @dec.skip_win32 def test_get_long_path_name(): - p = path.get_long_path_name('/usr/local') - nt.assert_equal(p,'/usr/local') + p = path.get_long_path_name("/usr/local") + assert p == "/usr/local" class TestRaiseDeprecation(unittest.TestCase): @@ -300,18 +300,18 @@ class TestRaiseDeprecation(unittest.TestCase): @with_environment def test_get_py_filename(): os.chdir(TMP_TEST_DIR) - with make_tempfile('foo.py'): - nt.assert_equal(path.get_py_filename('foo.py'), 'foo.py') - nt.assert_equal(path.get_py_filename('foo'), 'foo.py') - with make_tempfile('foo'): - nt.assert_equal(path.get_py_filename('foo'), 'foo') - nt.assert_raises(IOError, path.get_py_filename, 'foo.py') - nt.assert_raises(IOError, path.get_py_filename, 'foo') - nt.assert_raises(IOError, path.get_py_filename, 'foo.py') - true_fn = 'foo with spaces.py' + with make_tempfile("foo.py"): + assert path.get_py_filename("foo.py") == "foo.py" + assert path.get_py_filename("foo") == "foo.py" + with make_tempfile("foo"): + assert path.get_py_filename("foo") == "foo" + nt.assert_raises(IOError, path.get_py_filename, "foo.py") + nt.assert_raises(IOError, path.get_py_filename, "foo") + nt.assert_raises(IOError, path.get_py_filename, "foo.py") + true_fn = "foo with spaces.py" with make_tempfile(true_fn): - nt.assert_equal(path.get_py_filename('foo with spaces'), true_fn) - nt.assert_equal(path.get_py_filename('foo with spaces.py'), true_fn) + assert path.get_py_filename("foo with spaces") == true_fn + assert path.get_py_filename("foo with spaces.py") == true_fn nt.assert_raises(IOError, path.get_py_filename, '"foo with spaces.py"') nt.assert_raises(IOError, path.get_py_filename, "'foo with spaces.py'") @@ -361,8 +361,7 @@ class TestShellGlob(unittest.TestCase): def check_match(self, patterns, matches): with self.in_tempdir(): # glob returns unordered list. that's why sorted is required. - nt.assert_equal(sorted(path.shellglob(patterns)), - sorted(matches)) + assert sorted(path.shellglob(patterns)) == sorted(matches) def common_cases(self): return [ @@ -397,12 +396,13 @@ class TestShellGlob(unittest.TestCase): yield (self.check_match, patterns, matches) +# TODO : pytest.mark.parametrise once nose is gone. def test_unescape_glob(): - nt.assert_equal(path.unescape_glob(r'\*\[\!\]\?'), '*[!]?') - nt.assert_equal(path.unescape_glob(r'\\*'), r'\*') - nt.assert_equal(path.unescape_glob(r'\\\*'), r'\*') - nt.assert_equal(path.unescape_glob(r'\\a'), r'\a') - nt.assert_equal(path.unescape_glob(r'\a'), r'\a') + assert path.unescape_glob(r"\*\[\!\]\?") == "*[!]?" + assert path.unescape_glob(r"\\*") == r"\*" + assert path.unescape_glob(r"\\\*") == r"\*" + assert path.unescape_glob(r"\\a") == r"\a" + assert path.unescape_glob(r"\a") == r"\a" @onlyif_unicode_paths @@ -431,17 +431,19 @@ class TestLinkOrCopy(unittest.TestCase): return os.path.join(self.tempdir.name, *args) def assert_inode_not_equal(self, a, b): - nt.assert_not_equal(os.stat(a).st_ino, os.stat(b).st_ino, - "%r and %r do reference the same indoes" %(a, b)) + assert ( + os.stat(a).st_ino != os.stat(b).st_ino + ), "%r and %r do reference the same indoes" % (a, b) def assert_inode_equal(self, a, b): - nt.assert_equal(os.stat(a).st_ino, os.stat(b).st_ino, - "%r and %r do not reference the same indoes" %(a, b)) + assert ( + os.stat(a).st_ino == os.stat(b).st_ino + ), "%r and %r do not reference the same indoes" % (a, b) def assert_content_equal(self, a, b): with open(a) as a_f: with open(b) as b_f: - nt.assert_equal(a_f.read(), b_f.read()) + assert a_f.read() == b_f.read() @skip_win32 def test_link_successful(self): @@ -489,4 +491,4 @@ class TestLinkOrCopy(unittest.TestCase): path.link_or_copy(self.src, dst) path.link_or_copy(self.src, dst) self.assert_inode_equal(self.src, dst) - nt.assert_equal(sorted(os.listdir(self.tempdir.name)), ['src', 'target']) + assert sorted(os.listdir(self.tempdir.name)) == ["src", "target"] diff --git a/IPython/utils/tests/test_process.py b/IPython/utils/tests/test_process.py index fbc000c..c53999e 100644 --- a/IPython/utils/tests/test_process.py +++ b/IPython/utils/tests/test_process.py @@ -66,7 +66,8 @@ def test_find_cmd_fail(): """Make sure that FindCmdError is raised if we can't find the cmd.""" nt.assert_raises(FindCmdError,find_cmd,'asdfasdf') - + +# TODO: move to pytest.mark.parametrize once nose gone @dec.skip_win32 def test_arg_split(): """Ensure that argument lines are correctly split like in a shell.""" @@ -80,8 +81,10 @@ def test_arg_split(): ['something "with quotes"', ['something', '"with quotes"']], ] for argstr, argv in tests: - nt.assert_equal(arg_split(argstr), argv) - + assert arg_split(argstr) == argv + + +# TODO: move to pytest.mark.parametrize once nose gone @dec.skip_if_not_win32 def test_arg_split_win32(): """Ensure that argument lines are correctly split like in a shell.""" @@ -92,7 +95,7 @@ def test_arg_split_win32(): ['something "with quotes"', ['something', 'with quotes']], ] for argstr, argv in tests: - nt.assert_equal(arg_split(argstr), argv) + assert arg_split(argstr) == argv class SubProcessTestCase(tt.TempFileMixin): diff --git a/IPython/utils/tests/test_sysinfo.py b/IPython/utils/tests/test_sysinfo.py index c4f9c3c..c3f879b 100644 --- a/IPython/utils/tests/test_sysinfo.py +++ b/IPython/utils/tests/test_sysinfo.py @@ -12,6 +12,6 @@ from IPython.utils import sysinfo def test_json_getsysinfo(): """ - test that it is easily jsonable and don't return bytes somewhere. + test that it is easily jsonable and don't return bytes somewhere. """ json.dumps(sysinfo.get_sys_info()) diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py index 68474ae..8f9b73c 100644 --- a/IPython/utils/tests/test_text.py +++ b/IPython/utils/tests/test_text.py @@ -32,31 +32,31 @@ def test_columnize(): items = [l*size for l in 'abcd'] out = text.columnize(items, displaywidth=80) - nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n') + assert out == "aaaaa bbbbb ccccc ddddd\n" out = text.columnize(items, displaywidth=25) - nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + assert out == "aaaaa ccccc\nbbbbb ddddd\n" out = text.columnize(items, displaywidth=12) - nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + assert out == "aaaaa ccccc\nbbbbb ddddd\n" out = text.columnize(items, displaywidth=10) - nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n') + assert out == "aaaaa\nbbbbb\nccccc\nddddd\n" out = text.columnize(items, row_first=True, displaywidth=80) - nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n') + assert out == "aaaaa bbbbb ccccc ddddd\n" out = text.columnize(items, row_first=True, displaywidth=25) - nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n') + assert out == "aaaaa bbbbb\nccccc ddddd\n" out = text.columnize(items, row_first=True, displaywidth=12) - nt.assert_equal(out, 'aaaaa bbbbb\nccccc ddddd\n') + assert out == "aaaaa bbbbb\nccccc ddddd\n" out = text.columnize(items, row_first=True, displaywidth=10) - nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n') + assert out == "aaaaa\nbbbbb\nccccc\nddddd\n" out = text.columnize(items, displaywidth=40, spread=True) - nt.assert_equal(out, 'aaaaa bbbbb ccccc ddddd\n') + assert out == "aaaaa bbbbb ccccc ddddd\n" out = text.columnize(items, displaywidth=20, spread=True) - nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + assert out == "aaaaa ccccc\nbbbbb ddddd\n" out = text.columnize(items, displaywidth=12, spread=True) - nt.assert_equal(out, 'aaaaa ccccc\nbbbbb ddddd\n') + assert out == "aaaaa ccccc\nbbbbb ddddd\n" out = text.columnize(items, displaywidth=10, spread=True) - nt.assert_equal(out, 'aaaaa\nbbbbb\nccccc\nddddd\n') + assert out == "aaaaa\nbbbbb\nccccc\nddddd\n" def test_columnize_random(): @@ -77,38 +77,43 @@ def test_columnize_random(): print("size of each element :\n %s" % rand_len) assert False, "row_first={0}".format(row_first) + +# TODO: pytest mark.parametrize once nose removed. def test_columnize_medium(): """Test with inputs than shouldn't be wider than 80""" size = 40 items = [l*size for l in 'abc'] for row_first in [True, False]: out = text.columnize(items, row_first=row_first, displaywidth=80) - nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first)) + assert out == "\n".join(items + [""]), "row_first={0}".format(row_first) + +# TODO: pytest mark.parametrize once nose removed. def test_columnize_long(): """Test columnize with inputs longer than the display window""" size = 11 items = [l*size for l in 'abc'] for row_first in [True, False]: - out = text.columnize(items, row_first=row_first, displaywidth=size-1) - nt.assert_equal(out, '\n'.join(items+['']), "row_first={0}".format(row_first)) + out = text.columnize(items, row_first=row_first, displaywidth=size - 1) + assert out == "\n".join(items + [""]), "row_first={0}".format(row_first) + def eval_formatter_check(f): ns = dict(n=12, pi=math.pi, stuff='hello there', os=os, u=u"café", b="café") s = f.format("{n} {n//4} {stuff.split()[0]}", **ns) - nt.assert_equal(s, "12 3 hello") + assert s == "12 3 hello" s = f.format(' '.join(['{n//%i}'%i for i in range(1,8)]), **ns) - nt.assert_equal(s, "12 6 4 3 2 2 1") + assert s == "12 6 4 3 2 2 1" s = f.format('{[n//i for i in range(1,8)]}', **ns) - nt.assert_equal(s, "[12, 6, 4, 3, 2, 2, 1]") + assert s == "[12, 6, 4, 3, 2, 2, 1]" s = f.format("{stuff!s}", **ns) - nt.assert_equal(s, ns['stuff']) + assert s == ns["stuff"] s = f.format("{stuff!r}", **ns) - nt.assert_equal(s, repr(ns['stuff'])) - + assert s == repr(ns["stuff"]) + # Check with unicode: s = f.format("{u}", **ns) - nt.assert_equal(s, ns['u']) + assert s == ns["u"] # This decodes in a platform dependent manner, but it shouldn't error out s = f.format("{b}", **ns) @@ -117,25 +122,25 @@ def eval_formatter_check(f): def eval_formatter_slicing_check(f): ns = dict(n=12, pi=math.pi, stuff='hello there', os=os) s = f.format(" {stuff.split()[:]} ", **ns) - nt.assert_equal(s, " ['hello', 'there'] ") + assert s == " ['hello', 'there'] " s = f.format(" {stuff.split()[::-1]} ", **ns) - nt.assert_equal(s, " ['there', 'hello'] ") + assert s == " ['there', 'hello'] " s = f.format("{stuff[::2]}", **ns) - nt.assert_equal(s, ns['stuff'][::2]) - + assert s == ns["stuff"][::2] + nt.assert_raises(SyntaxError, f.format, "{n:x}", **ns) def eval_formatter_no_slicing_check(f): ns = dict(n=12, pi=math.pi, stuff='hello there', os=os) s = f.format('{n:x} {pi**2:+f}', **ns) - nt.assert_equal(s, "c +9.869604") - - s = f.format('{stuff[slice(1,4)]}', **ns) - nt.assert_equal(s, 'ell') + assert s == "c +9.869604" + + s = f.format("{stuff[slice(1,4)]}", **ns) + assert s == "ell" s = f.format("{a[:]}", a=[1, 2]) - nt.assert_equal(s, "[1, 2]") + assert s == "[1, 2]" def test_eval_formatter(): f = text.EvalFormatter() @@ -154,29 +159,16 @@ def test_dollar_formatter(): ns = dict(n=12, pi=math.pi, stuff='hello there', os=os) s = f.format("$n", **ns) - nt.assert_equal(s, "12") + assert s == "12" s = f.format("$n.real", **ns) - nt.assert_equal(s, "12") + assert s == "12" s = f.format("$n/{stuff[:5]}", **ns) - nt.assert_equal(s, "12/hello") + assert s == "12/hello" s = f.format("$n $$HOME", **ns) - nt.assert_equal(s, "12 $HOME") + assert s == "12 $HOME" s = f.format("${foo}", foo="HOME") - nt.assert_equal(s, "$HOME") - - -def test_long_substr(): - data = ['hi'] - nt.assert_equal(text.long_substr(data), 'hi') - - -def test_long_substr2(): - data = ['abc', 'abd', 'abf', 'ab'] - nt.assert_equal(text.long_substr(data), 'ab') + assert s == "$HOME" -def test_long_substr_empty(): - data = [] - nt.assert_equal(text.long_substr(data), '') def test_strip_email(): src = """\ @@ -189,25 +181,25 @@ def test_strip_email(): ... return x+1 ... >>> zz = f(2.5)""" - nt.assert_equal(text.strip_email_quotes(src), cln) + assert text.strip_email_quotes(src) == cln def test_strip_email2(): src = '> > > list()' cln = 'list()' - nt.assert_equal(text.strip_email_quotes(src), cln) + assert text.strip_email_quotes(src) == cln def test_LSString(): lss = text.LSString("abc\ndef") - nt.assert_equal(lss.l, ['abc', 'def']) - nt.assert_equal(lss.s, 'abc def') + assert lss.l == ["abc", "def"] + assert lss.s == "abc def" lss = text.LSString(os.getcwd()) nt.assert_is_instance(lss.p[0], Path) def test_SList(): - sl = text.SList(['a 11', 'b 1', 'a 2']) - nt.assert_equal(sl.n, 'a 11\nb 1\na 2') - nt.assert_equal(sl.s, 'a 11 b 1 a 2') - nt.assert_equal(sl.grep(lambda x: x.startswith('a')), text.SList(['a 11', 'a 2'])) - nt.assert_equal(sl.fields(0), text.SList(['a', 'b', 'a'])) - nt.assert_equal(sl.sort(field=1, nums=True), text.SList(['b 1', 'a 2', 'a 11'])) + sl = text.SList(["a 11", "b 1", "a 2"]) + assert sl.n == "a 11\nb 1\na 2" + assert sl.s == "a 11 b 1 a 2" + assert sl.grep(lambda x: x.startswith("a")) == text.SList(["a 11", "a 2"]) + assert sl.fields(0) == text.SList(["a", "b", "a"]) + assert sl.sort(field=1, nums=True) == text.SList(["b 1", "a 2", "a 11"]) diff --git a/IPython/utils/text.py b/IPython/utils/text.py index 194107a..c25869a 100644 --- a/IPython/utils/text.py +++ b/IPython/utils/text.py @@ -10,6 +10,7 @@ Inheritance diagram: import os import re +import string import sys import textwrap from string import Formatter @@ -252,7 +253,6 @@ def indent(instr,nspaces=4, ntabs=0, flatten=False): Parameters ---------- - instr : basestring The string to be indented. nspaces : int (default: 4) @@ -266,7 +266,6 @@ def indent(instr,nspaces=4, ntabs=0, flatten=False): Returns ------- - str|unicode : string indented by ntabs and nspaces. """ @@ -390,7 +389,6 @@ def wrap_paragraphs(text, ncols=80): Returns ------- - list of complete paragraphs, wrapped to fill `ncols` columns. """ paragraph_re = re.compile(r'\n(\s*\n)+', re.MULTILINE) @@ -408,22 +406,6 @@ def wrap_paragraphs(text, ncols=80): return out_ps -def long_substr(data): - """Return the longest common substring in a list of strings. - - Credit: http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python - """ - substr = '' - if len(data) > 1 and len(data[0]) > 0: - for i in range(len(data[0])): - for j in range(len(data[0])-i+1): - if j > len(substr) and all(data[0][i:i+j] in x for x in data): - substr = data[0][i:i+j] - elif len(data) == 1: - substr = data[0] - return substr - - def strip_email_quotes(text): """Strip leading email quotation characters ('>'). @@ -450,31 +432,34 @@ def strip_email_quotes(text): In [4]: strip_email_quotes('> > text\\n> > more\\n> more...') Out[4]: '> text\\n> more\\nmore...' - So if any line has no quote marks ('>') , then none are stripped from any + So if any line has no quote marks ('>'), then none are stripped from any of them :: - + In [5]: strip_email_quotes('> > text\\n> > more\\nlast different') Out[5]: '> > text\\n> > more\\nlast different' """ lines = text.splitlines() - matches = set() - for line in lines: - prefix = re.match(r'^(\s*>[ >]*)', line) - if prefix: - matches.add(prefix.group(1)) + strip_len = 0 + + for characters in zip(*lines): + # Check if all characters in this position are the same + if len(set(characters)) > 1: + break + prefix_char = characters[0] + + if prefix_char in string.whitespace or prefix_char == ">": + strip_len += 1 else: break - else: - prefix = long_substr(list(matches)) - if prefix: - strip = len(prefix) - text = '\n'.join([ ln[strip:] for ln in lines]) + + text = "\n".join([ln[strip_len:] for ln in lines]) return text + def strip_ansi(source): """ Remove ansi escape codes from text. - + Parameters ---------- source : str @@ -651,7 +636,6 @@ def compute_item_matrix(items, row_first=False, empty=None, *args, **kwargs) : Parameters ---------- - items list of strings to columize row_first : (default False) @@ -666,14 +650,11 @@ def compute_item_matrix(items, row_first=False, empty=None, *args, **kwargs) : Returns ------- - strings_matrix - nested list of string, the outer most list contains as many list as rows, the innermost lists have each as many element as columns. If the total number of elements in `items` does not equal the product of rows*columns, the last element of some lists are filled with `None`. - dict_info some info to make columnize easier: @@ -713,14 +694,11 @@ def columnize(items, row_first=False, separator=' ', displaywidth=80, spread=Fa ---------- items : sequence of strings The strings to process. - row_first : (default False) Whether to compute columns for a row-first matrix instead of column-first (default). - separator : str, optional [default is two spaces] The string that separates columns. - displaywidth : int, optional [default is 80] Width of the display in number of characters. diff --git a/IPython/utils/timing.py b/IPython/utils/timing.py index 9a31aff..32741ac 100644 --- a/IPython/utils/timing.py +++ b/IPython/utils/timing.py @@ -23,6 +23,11 @@ import time # If possible (Unix), use the resource module instead of time.clock() try: import resource +except ImportError: + resource = None + +# Some implementations (like jyputerlite) don't have getrusage +if resource is not None and hasattr(resource, "getrusage"): def clocku(): """clocku() -> floating point number @@ -56,7 +61,9 @@ try: Similar to clock(), but return a tuple of user/system times.""" return resource.getrusage(resource.RUSAGE_SELF)[:2] -except ImportError: + + +else: # There is no distinction of user/system time under windows, so we just use # time.perff_counter() for everything... clocku = clocks = clock = time.perf_counter diff --git a/IPython/utils/tokenutil.py b/IPython/utils/tokenutil.py index 28f8b6d..697d2b5 100644 --- a/IPython/utils/tokenutil.py +++ b/IPython/utils/tokenutil.py @@ -23,20 +23,18 @@ def generate_tokens(readline): def line_at_cursor(cell, cursor_pos=0): """Return the line in a cell at a given cursor position - + Used for calling line-based APIs that don't support multi-line input, yet. - + Parameters ---------- - - cell: str + cell : str multiline block of text - cursor_pos: integer + cursor_pos : integer the cursor position - + Returns ------- - (line, offset): (string, integer) The line with the current cursor, and the character offset of the start of the line. """ @@ -58,15 +56,14 @@ def line_at_cursor(cell, cursor_pos=0): def token_at_cursor(cell, cursor_pos=0): """Get the token at a given cursor - + Used for introspection. - + Function calls are prioritized, so the token for the callable will be returned if the cursor is anywhere inside the call. - + Parameters ---------- - cell : unicode A block of Python code cursor_pos : int diff --git a/README.rst b/README.rst index 788629b..2db6b81 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ .. image:: https://img.shields.io/pypi/v/IPython.svg :target: https://pypi.python.org/pypi/ipython -.. image:: https://img.shields.io/travis/ipython/ipython.svg - :target: https://travis-ci.org/ipython/ipython +.. image:: https://github.com/ipython/ipython/actions/workflows/test.yml/badge.svg + :target: https://github.com/ipython/ipython/actions/workflows/test.yml) .. image:: https://www.codetriage.com/ipython/ipython/badges/users.svg :target: https://www.codetriage.com/ipython/ipython/ diff --git a/docs/requirements.txt b/docs/requirements.txt index b3ff528..93e162f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ --e . +-e .[test] ipykernel setuptools>=18.5 sphinx @@ -6,3 +6,4 @@ sphinx-rtd-theme docrepr matplotlib stack_data +pytest diff --git a/docs/source/_templates/breadcrumbs.html b/docs/source/_templates/breadcrumbs.html new file mode 100644 index 0000000..804ad69 --- /dev/null +++ b/docs/source/_templates/breadcrumbs.html @@ -0,0 +1,7 @@ +{%- extends "sphinx_rtd_theme/breadcrumbs.html" %} + +{% block breadcrumbs_aside %} +{% if not meta or meta.get('github_url') != 'hide' %} +{{ super() }} +{% endif %} +{% endblock %} diff --git a/docs/source/conf.py b/docs/source/conf.py index eb5b39e..2312cc2 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,7 +18,7 @@ import sys, os from pathlib import Path -# http://read-the-docs.readthedocs.io/en/latest/faq.html +# https://read-the-docs.readthedocs.io/en/latest/faq.html ON_RTD = os.environ.get('READTHEDOCS', None) == 'True' if ON_RTD: diff --git a/docs/source/config/custommagics.rst b/docs/source/config/custommagics.rst index f4daa9a..99d4068 100644 --- a/docs/source/config/custommagics.rst +++ b/docs/source/config/custommagics.rst @@ -151,7 +151,7 @@ Complete Example Here is a full example of a magic package. You can distribute magics using setuptools, distutils, or any other distribution tools like `flit -`_ for pure Python packages. +`_ for pure Python packages. When distributing magics as part of a package, recommended best practice is to execute the registration inside the `load_ipython_extension` as demonstrated in diff --git a/docs/source/config/eventloops.rst b/docs/source/config/eventloops.rst index 04bdd9f..dd527a6 100644 --- a/docs/source/config/eventloops.rst +++ b/docs/source/config/eventloops.rst @@ -7,9 +7,9 @@ loop, so you can use both a GUI and an interactive prompt together. IPython supports a number of common GUI toolkits, but from IPython 3.0, it is possible to integrate other event loops without modifying IPython itself. -Supported event loops include ``qt4``, ``qt5``, ``gtk2``, ``gtk3``, ``wx``, -``osx`` and ``tk``. Make sure the event loop you specify matches the GUI -toolkit used by your own code. +Supported event loops include ``qt4``, ``qt5``, ``gtk2``, ``gtk3``, ``gtk4``, +``wx``, ``osx`` and ``tk``. Make sure the event loop you specify matches the +GUI toolkit used by your own code. To make IPython GUI event loop integration occur automatically at every startup, set the ``c.InteractiveShellApp.gui`` configuration key in your diff --git a/docs/source/config/integrating.rst b/docs/source/config/integrating.rst index 0a1f0ec..a045968 100644 --- a/docs/source/config/integrating.rst +++ b/docs/source/config/integrating.rst @@ -24,14 +24,59 @@ returns a list of objects which are possible keys in a subscript expression Rich display ============ -The notebook and the Qt console can display richer representations of objects. -To use this, you can define any of a number of ``_repr_*_()`` methods. Note that -these are surrounded by single, not double underscores. - -Both the notebook and the Qt console can display ``svg``, ``png`` and ``jpeg`` -representations. The notebook can also display ``html``, ``javascript``, -``markdown`` and ``latex``. If the methods don't exist, or return ``None``, it -falls back to a standard ``repr()``. +Custom methods +---------------------- +IPython can display richer representations of objects. +To do this, you can define ``_ipython_display_()``, or any of a number of +``_repr_*_()`` methods. +Note that these are surrounded by single, not double underscores. + +.. list-table:: Supported ``_repr_*_`` methods + :widths: 20 15 15 15 + :header-rows: 1 + + * - Format + - REPL + - Notebook + - Qt Console + * - ``_repr_pretty_`` + - yes + - yes + - yes + * - ``_repr_svg_`` + - no + - yes + - yes + * - ``_repr_png_`` + - no + - yes + - yes + * - ``_repr_jpeg_`` + - no + - yes + - yes + * - ``_repr_html_`` + - no + - yes + - no + * - ``_repr_javascript_`` + - no + - yes + - no + * - ``_repr_markdown_`` + - no + - yes + - no + * - ``_repr_latex_`` + - no + - yes + - no + * - ``_repr_mimebundle_`` + - no + - ? + - ? + +If the methods don't exist, or return ``None``, the standard ``repr()`` is used. For example:: @@ -42,43 +87,61 @@ For example:: def _repr_html_(self): return "

" + self.text + "

" -We often want to provide frontends with guidance on how to display the data. To -support this, ``_repr_*_()`` methods can also return a ``(data, metadata)`` -tuple where ``metadata`` is a dictionary containing arbitrary key-value pairs for -the frontend to interpret. An example use case is ``_repr_jpeg_()``, which can -be set to return a jpeg image and a ``{'height': 400, 'width': 600}`` dictionary -to inform the frontend how to size the image. -There are also two more powerful display methods: +Special methods +^^^^^^^^^^^^^^^ + +Pretty printing +""""""""""""""" + +To customize how your object is pretty-printed, add a ``_repr_pretty_`` method +to the class. +The method should accept a pretty printer, and a boolean that indicates whether +the printer detected a cycle. +The method should act on the printer to produce your customized pretty output. +Here is an example:: + + class MyObject(object): + + def _repr_pretty_(self, p, cycle): + if cycle: + p.text('MyObject(...)') + else: + p.text('MyObject[...]') + +For details on how to use the pretty printer, see :py:mod:`IPython.lib.pretty`. + +More powerful methods +""""""""""""""""""""" .. class:: MyObject .. method:: _repr_mimebundle_(include=None, exclude=None) Should return a dictionary of multiple formats, keyed by mimetype, or a tuple - of two dictionaries: *data, metadata*. If this returns something, other - ``_repr_*_`` methods are ignored. The method should take keyword arguments - ``include`` and ``exclude``, though it is not required to respect them. + of two dictionaries: *data, metadata* (see :ref:`Metadata`). + If this returns something, other ``_repr_*_`` methods are ignored. + The method should take keyword arguments ``include`` and ``exclude``, though + it is not required to respect them. .. method:: _ipython_display_() Displays the object as a side effect; the return value is ignored. If this is defined, all other display methods are ignored. + This method is ignored in the REPL. -To customize how the REPL pretty-prints your object, add a `_repr_pretty_` -method to the class. The method should accept a pretty printer, and a boolean -that indicates whether the printer detected a cycle. The method should act on -the printer to produce your customized pretty output. Here is an example:: - class MyObject(object): +Metadata +^^^^^^^^ + +We often want to provide frontends with guidance on how to display the data. To +support this, ``_repr_*_()`` methods (except `_repr_pretty_``?) can also return a ``(data, metadata)`` +tuple where ``metadata`` is a dictionary containing arbitrary key-value pairs for +the frontend to interpret. An example use case is ``_repr_jpeg_()``, which can +be set to return a jpeg image and a ``{'height': 400, 'width': 600}`` dictionary +to inform the frontend how to size the image. - def _repr_pretty_(self, p, cycle): - if cycle: - p.text('MyObject(...)') - else: - p.text('MyObject[...]') -For details, see :py:mod:`IPython.lib.pretty`. Formatters for third-party types -------------------------------- @@ -103,7 +166,7 @@ Rarely, you might want to display a custom traceback when reporting an exception. To do this, define the custom traceback using `_render_traceback_(self)` method which returns a list of strings, one string for each line of the traceback. For example, the `ipyparallel -`__ a parallel computing framework for +`__ a parallel computing framework for IPython, does this to display errors from multiple engines. Please be conservative in using this feature; by replacing the default traceback diff --git a/docs/source/development/config.rst b/docs/source/development/config.rst index 7623a7e..0d52a67 100644 --- a/docs/source/development/config.rst +++ b/docs/source/development/config.rst @@ -119,11 +119,6 @@ which adds a directory called ``profile_`` to your IPython directory. Then you can load this profile by adding ``--profile=`` to your command line options. Profiles are supported by all IPython applications. -IPython ships with some sample profiles in :file:`IPython/config/profile`. If -you create profiles with the name of one of our shipped profiles, these config -files will be copied over instead of starting with the automatically generated -config files. - IPython extends the config loader for Python files so that you can inherit config from another profile. To do this, use a line like this in your Python config file: diff --git a/docs/source/development/parallel_connections.rst b/docs/source/development/parallel_connections.rst index db1cccf..7253273 100644 --- a/docs/source/development/parallel_connections.rst +++ b/docs/source/development/parallel_connections.rst @@ -5,4 +5,4 @@ Connection Diagrams of The IPython ZMQ Cluster ============================================== IPython parallel has moved to ipyparallel - -see :ref:`ipyparallel:parallel_connections` for the documentation. +see :ref:`ipyparallel:/reference/connections.md` for the documentation. diff --git a/docs/source/development/parallel_messages.rst b/docs/source/development/parallel_messages.rst index f37ea93..180c311 100644 --- a/docs/source/development/parallel_messages.rst +++ b/docs/source/development/parallel_messages.rst @@ -5,4 +5,4 @@ Messaging for Parallel Computing ================================ IPython parallel has moved to ipyparallel - -see :ref:`ipyparallel:parallel_messages` for the documentation. +see :ref:`ipyparallel:/reference/messages.md` for the documentation. diff --git a/docs/source/development/wrapperkernels.rst b/docs/source/development/wrapperkernels.rst index d734c30..a15cf8e 100644 --- a/docs/source/development/wrapperkernels.rst +++ b/docs/source/development/wrapperkernels.rst @@ -7,7 +7,7 @@ You can now re-use the kernel machinery in IPython to easily make new kernels. This is useful for languages that have Python bindings, such as `Octave `_ (via `Oct2Py `_), or languages -where the REPL can be controlled in a tty using `pexpect `_, +where the REPL can be controlled in a tty using `pexpect `_, such as bash. .. seealso:: diff --git a/docs/source/index.rst b/docs/source/index.rst index c627993..9cec057 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -57,7 +57,7 @@ features: The Command line interface inherits the above functionality and adds -* real multi-line editing thanks to `prompt_toolkit `_. +* real multi-line editing thanks to `prompt_toolkit `_. * syntax highlighting as you type. @@ -69,7 +69,7 @@ it allows: * the object to create a rich display of Html, Images, Latex, Sound and Video. -* interactive widgets with the use of the `ipywidgets `_ package. +* interactive widgets with the use of the `ipywidgets `_ package. This documentation will walk you through most of the features of the IPython @@ -102,9 +102,9 @@ repository `_. .. seealso:: - `Jupyter documentation `__ + `Jupyter documentation `__ The Jupyter documentation provides information about the Notebook code and other Jupyter sub-projects. - `ipyparallel documentation `__ + `ipyparallel documentation `__ Formerly ``IPython.parallel``. diff --git a/docs/source/install/index.rst b/docs/source/install/index.rst index 3793fae..41036d8 100644 --- a/docs/source/install/index.rst +++ b/docs/source/install/index.rst @@ -51,7 +51,7 @@ for more help see .. seealso:: - `Installing Jupyter `__ + `Installing Jupyter `__ The Notebook, nbconvert, and many other former pieces of IPython are now part of Project Jupyter. diff --git a/docs/source/interactive/index.rst b/docs/source/interactive/index.rst index 7010c51..6684eb5 100644 --- a/docs/source/interactive/index.rst +++ b/docs/source/interactive/index.rst @@ -29,4 +29,4 @@ done some work in the classic Python REPL. .. seealso:: `A Qt Console for Jupyter `__ - `The Jupyter Notebook `__ + `The Jupyter Notebook `__ diff --git a/docs/source/interactive/python-ipython-diff.rst b/docs/source/interactive/python-ipython-diff.rst index 525475d..564c1c5 100644 --- a/docs/source/interactive/python-ipython-diff.rst +++ b/docs/source/interactive/python-ipython-diff.rst @@ -3,20 +3,20 @@ Python vs IPython ================= This document is meant to highlight the main differences between the Python -language and what are the specific construct you can do only in IPython. +language and what are the specific constructs you can do only in IPython. -Unless expressed otherwise all of the construct you will see here will raise a +Unless expressed otherwise all of the constructs you will see here will raise a ``SyntaxError`` if run in a pure Python shell, or if executing in a Python script. -Each of these features are described more in detail in further part of the documentation. +Each of these features is described more in detail in the further parts of the documentation. Quick overview: =============== -All the following construct are valid IPython syntax: +All the following constructs are valid IPython syntax: .. code-block:: ipython @@ -46,9 +46,9 @@ All the following construct are valid IPython syntax: .. code-block:: ipython In [1]: my_files = !ls ~/ - In [1]: for i,file in enumerate(my_file): + In [1]: for i, file in enumerate(my_files): ...: raw = !echo $file - ...: !echo {files[0].upper()} $raw + ...: !echo {file[0].upper()} $raw .. code-block:: ipython @@ -58,8 +58,8 @@ All the following construct are valid IPython syntax: ...: print $months[0]; -Each of these construct is compiled by IPython into valid python code and will -do most of the time what you expect it will do. Let see each of these example +Each of these constructs is compiled by IPython into valid python code and will +do most of the time what you expect it will do. Let's see each of these examples in more detail. @@ -89,7 +89,7 @@ shortcut to get help. A question mark alone will bring up the IPython help: ------------- ... -A single question mark before, or after an object available in current +A single question mark before or after an object available in the current namespace will show help relative to this object: .. code-block:: ipython @@ -127,7 +127,7 @@ and if possible display the python source code of this object. If you are looking for an object, the use of wildcards ``*`` in conjunction -with question mark will allow you to search current namespace for object with +with a question mark will allow you to search the current namespace for objects with matching names: .. code-block:: ipython @@ -142,10 +142,10 @@ Shell Assignment ================ -When doing interactive computing it is common to need to access the underlying shell. +When doing interactive computing it is a common need to access the underlying shell. This is doable through the use of the exclamation mark ``!`` (or bang). -This allow to execute simple command when present in beginning of line: +This allows to execute simple commands when present in beginning of the line: .. code-block:: ipython @@ -167,7 +167,7 @@ Or edit file: The line after the bang can call any program installed in the underlying shell, and support variable expansion in the form of ``$variable`` or ``{variable}``. -The later form of expansion supports arbitrary python expression: +The later form of expansion supports arbitrary python expressions: .. code-block:: ipython @@ -176,45 +176,44 @@ The later form of expansion supports arbitrary python expression: In[2]: !mv $file {file.upper()} -The bang can also be present in the right hand side of an assignment, just -after the equal sign, or separated from it by a white space. In which case the -standard output of the command after the bang ``!`` will be split out into lines -in a list-like object and assign to the left hand side. +The bang (``!``) can also be present on the right hand side of an assignment, just +after the equal sign, or separated from it by a white space. In this case the +standard output of the command after the bang will be split out into lines +in a list-like object and assigned to the left hand side. -This allow you for example to put the list of files of the current working directory in a variable: +This allows you, for example, to put the list of files of the current working directory in a variable: .. code-block:: ipython In[1]: my_files = !ls -You can combine the different possibilities in for loops, condition, functions...: +You can combine the different possibilities in for loops, conditions, functions...: .. code-block:: ipython my_files = !ls ~/ - b = "backup file" - for i,file in enumerate(my_file): + for i, file in enumerate(my_files): raw = !echo $backup $file - !cp $file {file.split('.')[0]+'.bak'} + !cp $file {file.split('.')[0] + '.bak'} Magics ------ -Magics function are often present in the form of shell-like syntax, but are -under the hood python function. The syntax and assignment possibility are +Magic functions (magics) are often present in the form of shell-like syntax, but they are +python functions under the hood. The syntax and assignment possibilities are similar to the one with the bang (``!``) syntax, but with more flexibility and -power. Magic function start with a percent sign (``%``) or double percent (``%%``). +power. Magic functions start with a percent sign (``%``) or double percent signs (``%%``). -A magic call with a sign percent will act only on one line: +A magic call with a single percent sign will act only on one line: .. code-block:: ipython In[1]: %xmode Exception reporting mode: Verbose -And support assignment: +Magics support assignment: .. code-block:: ipython @@ -224,7 +223,7 @@ And support assignment: In [2]: results Out[2]: -Magic with two percent sign can spread over multiple lines, but does not support assignment: +Magics with double percent signs (``%%``) can spread over multiple lines, but they do not support assignments: .. code-block:: ipython @@ -239,11 +238,3 @@ Magic with two percent sign can spread over multiple lines, but does not support devfs 190Ki 190Ki 0Bi 100% 656 0 100% /dev map -hosts 0Bi 0Bi 0Bi 100% 0 0 100% /net map auto_home 0Bi 0Bi 0Bi 100% 0 0 100% /hom - - -Combining it all ----------------- - -:: - - find a snippet that combine all that into one thing! diff --git a/docs/source/interactive/reference.rst b/docs/source/interactive/reference.rst index e89fec4..8eed534 100644 --- a/docs/source/interactive/reference.rst +++ b/docs/source/interactive/reference.rst @@ -11,8 +11,11 @@ You start IPython with the command:: $ ipython [options] files -If invoked with no options, it executes all the files listed in sequence and -exits. If you add the ``-i`` flag, it drops you into the interpreter while still +If invoked with no options, it executes the file and exits, passing the +remaining arguments to the script, just as if you had specified the same +command with python. You may need to specify `--` before args to be passed +to the script, to prevent IPython from attempting to parse them. +If you add the ``-i`` flag, it drops you into the interpreter while still acknowledging any options you may have set in your ``ipython_config.py``. This behavior is different from standard Python, which when called as python ``-i`` will only execute one file and ignore your configuration setup. @@ -41,7 +44,7 @@ the command-line by passing the full class name and a corresponding value; type <...snip...> --matplotlib= (InteractiveShellApp.matplotlib) Default: None - Choices: ['auto', 'gtk', 'gtk3', 'inline', 'nbagg', 'notebook', 'osx', 'qt', 'qt4', 'qt5', 'tk', 'wx'] + Choices: ['auto', 'gtk', 'gtk3', 'gtk4', 'inline', 'nbagg', 'notebook', 'osx', 'qt', 'qt4', 'qt5', 'tk', 'wx'] Configure matplotlib for interactive use with the default matplotlib backend. <...snip...> @@ -899,7 +902,8 @@ For users, enabling GUI event loop integration is simple. You simple use the %gui [GUINAME] With no arguments, ``%gui`` removes all GUI support. Valid ``GUINAME`` -arguments include ``wx``, ``qt``, ``qt5``, ``gtk``, ``gtk3`` and ``tk``. +arguments include ``wx``, ``qt``, ``qt5``, ``gtk``, ``gtk3`` ``gtk4``, and +``tk``. Thus, to use wxPython interactively and create a running :class:`wx.App` object, do:: diff --git a/docs/source/interactive/shell.rst b/docs/source/interactive/shell.rst index 3c72702..0ea125c 100644 --- a/docs/source/interactive/shell.rst +++ b/docs/source/interactive/shell.rst @@ -1,5 +1,11 @@ .. _ipython_as_shell: +.. note:: + + This page has been kept for historical reason. You most likely want to use + `Xonsh `__ instead of this. + + ========================= IPython as a system shell ========================= @@ -44,6 +50,10 @@ so you should be able to type any normal system command and have it executed. See ``%alias?`` and ``%unalias?`` for details on the alias facilities. See also ``%rehashx?`` for details on the mechanism used to load $PATH. +.. warning:: + + See info at the top of the page. You most likely want to use + `Xonsh `__ instead of this. Directory management ==================== @@ -196,3 +206,9 @@ provide a convenient ways to use contained text in different formats: * ``.s`` returns string with lines separated by single space (for convenient passing to system commands) * ``.p`` returns list of "path" objects from detected file names + +.. error:: + + You went too far scroll back up. You most likely want to use + `Xonsh `__ instead of this. + diff --git a/docs/source/interactive/tutorial.rst b/docs/source/interactive/tutorial.rst index e1000e0..511e746 100644 --- a/docs/source/interactive/tutorial.rst +++ b/docs/source/interactive/tutorial.rst @@ -90,7 +90,7 @@ completion also works on file and directory names. Starting with IPython 6.0, if ``jedi`` is installed, IPython will try to pull completions from Jedi as well. This allows to not only inspect currently existing objects, but also to infer completion statically without executing -code. There is nothing particular need to get this to work, simply use tab +code. There is nothing particular needed to get this to work, simply use tab completion on more complex expressions like the following:: >>> data = ['Number of users', 123456] diff --git a/docs/source/overview.rst b/docs/source/overview.rst index fa70a2f..2c5920e 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -219,7 +219,7 @@ different numbers which correspond to the Process ID of the kernel. You can read more about using `jupyter qtconsole `_, and -`jupyter notebook `_. There +`jupyter notebook `_. There is also a :ref:`message spec ` which documents the protocol for communication between kernels and clients. @@ -234,7 +234,7 @@ Interactive parallel computing This functionality is optional and now part of the `ipyparallel -`_ project. +`_ project. Portability and Python requirements ----------------------------------- diff --git a/docs/source/whatsnew/development.rst b/docs/source/whatsnew/development.rst index 3fb260c..c092a6a 100644 --- a/docs/source/whatsnew/development.rst +++ b/docs/source/whatsnew/development.rst @@ -152,6 +152,24 @@ and "??", in much the same way it can be done when using the IPython prompt:: Previously, "pinfo" or "pinfo2" command had to be used for this purpose. + +Autoreload 3 feature +==================== + +Example: When an IPython session is ran with the 'autoreload' extension loaded, +you will now have the option '3' to select which means the following: + + 1. replicate all functionality from option 2 + 2. autoload all new funcs/classes/enums/globals from the module when they're added + 3. autoload all newly imported funcs/classes/enums/globals from external modules + +Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload`` + +For more information please see unit test - + extensions/tests/test_autoreload.py : 'test_autoload_newly_added_objects' + +======= + .. DO NOT EDIT THIS LINE BEFORE RELEASE. FEATURE INSERTION POINT. As a reminder, IPython master has diverged from the 7.x branch, thus master may diff --git a/docs/source/whatsnew/github-stats-1.0.rst b/docs/source/whatsnew/github-stats-1.0.rst index 03622ec..4aeb36e 100644 --- a/docs/source/whatsnew/github-stats-1.0.rst +++ b/docs/source/whatsnew/github-stats-1.0.rst @@ -1099,7 +1099,7 @@ Pull Requests (793): * :ghpull:`2274`: CLN: Use name to id mapping of notebooks instead of searching. * :ghpull:`2270`: SSHLauncher tweaks * :ghpull:`2269`: add missing location when disambiguating controller IP -* :ghpull:`2263`: Allow docs to build on http://readthedocs.io/ +* :ghpull:`2263`: Allow docs to build on https://readthedocs.io/ * :ghpull:`2256`: Adding data publication example notebook. * :ghpull:`2255`: better flush iopub with AsyncResults * :ghpull:`2261`: Fix: longest_substr([]) -> '' diff --git a/docs/source/whatsnew/pr/empty-hist-range.rst b/docs/source/whatsnew/pr/empty-hist-range.rst new file mode 100644 index 0000000..a36789f --- /dev/null +++ b/docs/source/whatsnew/pr/empty-hist-range.rst @@ -0,0 +1,19 @@ +Empty History Ranges +==================== + +A number of magics that take history ranges can now be used with an empty +range. These magics are: + + * ``%save`` + * ``%load`` + * ``%pastebin`` + * ``%pycat`` + +Using them this way will make them take the history of the current session up +to the point of the magic call (such that the magic itself will not be +included). + +Therefore it is now possible to save the whole history to a file using simple +``%save ``, load and edit it using ``%load`` (makes for a nice usage +when followed with :kbd:`F2`), send it to dpaste.org using ``%pastebin``, or +view the whole thing syntax-highlighted with a single ``%pycat``. diff --git a/docs/source/whatsnew/pr/enable-to-add-extra-attrs-to-iframe.rst b/docs/source/whatsnew/pr/enable-to-add-extra-attrs-to-iframe.rst new file mode 100644 index 0000000..1954bc4 --- /dev/null +++ b/docs/source/whatsnew/pr/enable-to-add-extra-attrs-to-iframe.rst @@ -0,0 +1,38 @@ +``YouTubeVideo`` autoplay and the ability to add extra attributes to ``IFrame`` +=============================================================================== + +You can add any extra attributes to the `` + +Related to the above, the ``YouTubeVideo`` class now takes an +``allow_autoplay`` flag, which sets up the iframe of the embedded YouTube video +such that it allows autoplay. + +.. note:: + Whether this works depends on the autoplay policy of the browser rendering + the HTML allowing it. It also could get blocked by some browser extensions. + +Try it out! +:: + + In [1]: from IPython.display import YouTubeVideo + + In [2]: YouTubeVideo("dQw4w9WgXcQ", allow_autoplay=True) + +🙃 diff --git a/docs/source/whatsnew/pr/ipdb-interact.rst b/docs/source/whatsnew/pr/ipdb-interact.rst new file mode 100644 index 0000000..8783be6 --- /dev/null +++ b/docs/source/whatsnew/pr/ipdb-interact.rst @@ -0,0 +1,4 @@ +IPython shell for ipdb interact +------------------------------- + +The ipdb ``interact`` starts an IPython shell instead of Python's built-in ``code.interact()``. diff --git a/docs/source/whatsnew/pr/pastebin-expiry-days.rst b/docs/source/whatsnew/pr/pastebin-expiry-days.rst new file mode 100644 index 0000000..68faa78 --- /dev/null +++ b/docs/source/whatsnew/pr/pastebin-expiry-days.rst @@ -0,0 +1,7 @@ +Pastebin magic expiry days option +================================= + +The Pastebin magic now has ``-e`` option to determine +the number of days for paste expiration. For example +the paste that created with ``%pastebin -e 20 1`` magic will +be available for next 20 days. diff --git a/docs/source/whatsnew/pr/sunken-brackets.rst b/docs/source/whatsnew/pr/sunken-brackets.rst new file mode 100644 index 0000000..f64f936 --- /dev/null +++ b/docs/source/whatsnew/pr/sunken-brackets.rst @@ -0,0 +1,7 @@ +Don't start a multiline cell with sunken parenthesis +---------------------------------------------------- + +From now on IPython will not ask for the next line of input when given a single +line with more closing than opening brackets. For example, this means that if +you (mis)type ']]' instead of '[]', a ``SyntaxError`` will show up, instead of +the ``...:`` prompt continuation. diff --git a/docs/source/whatsnew/pr/traceback-improvements.rst b/docs/source/whatsnew/pr/traceback-improvements.rst new file mode 100644 index 0000000..7040f58 --- /dev/null +++ b/docs/source/whatsnew/pr/traceback-improvements.rst @@ -0,0 +1,39 @@ +Traceback improvements +====================== + +Previously, error tracebacks for errors happening in code cells were showing a hash, the one used for compiling the Python AST:: + + In [1]: def foo(): + ...: return 3 / 0 + ...: + + In [2]: foo() + --------------------------------------------------------------------------- + ZeroDivisionError Traceback (most recent call last) + in + ----> 1 foo() + + in foo() + 1 def foo(): + ----> 2 return 3 / 0 + 3 + + ZeroDivisionError: division by zero + +The error traceback is now correctly formatted, showing the cell number in which the error happened:: + + In [1]: def foo(): + ...: return 3 / 0 + ...: + + In [2]: foo() + --------------------------------------------------------------------------- + ZeroDivisionError Traceback (most recent call last) + In [2], in + ----> 1 foo() + + In [1], in foo() + 1 def foo(): + ----> 2 return 3 / 0 + + ZeroDivisionError: division by zero diff --git a/docs/source/whatsnew/pr/vi-prompt-strip.rst b/docs/source/whatsnew/pr/vi-prompt-strip.rst new file mode 100644 index 0000000..e642a4d --- /dev/null +++ b/docs/source/whatsnew/pr/vi-prompt-strip.rst @@ -0,0 +1,29 @@ +Automatic Vi prompt stripping +============================= + +When pasting code into IPython, it will strip the leading prompt characters if +there are any. For example, you can paste the following code into the console - +it will still work, even though each line is prefixed with prompts (`In`, +`Out`):: + + In [1]: 2 * 2 == 4 + Out[1]: True + + In [2]: print("This still works as pasted") + + +Previously, this was not the case for the Vi-mode prompts:: + + In [1]: [ins] In [13]: 2 * 2 == 4 + ...: Out[13]: True + ...: + File "", line 1 + [ins] In [13]: 2 * 2 == 4 + ^ + SyntaxError: invalid syntax + +This is now fixed, and Vi prompt prefixes - ``[ins]`` and ``[nav]`` - are +skipped just as the normal ``In`` would be. + +IPython shell can be started in the Vi mode using ``ipython +--TerminalInteractiveShell.editing_mode=vi`` diff --git a/docs/source/whatsnew/version0.11.rst b/docs/source/whatsnew/version0.11.rst index 05d6447..fc35ae5 100644 --- a/docs/source/whatsnew/version0.11.rst +++ b/docs/source/whatsnew/version0.11.rst @@ -309,7 +309,7 @@ be started by calling ``ipython qtconsole``. The protocol is :ref:`documented `. The parallel computing framework has also been rewritten using ZMQ. The -protocol is described :ref:`here `, and the code is in the +protocol is described :ref:`here `, and the code is in the new :mod:`IPython.parallel` module. .. _python3_011: diff --git a/docs/source/whatsnew/version5.rst b/docs/source/whatsnew/version5.rst index 62aea16..61b6ca0 100644 --- a/docs/source/whatsnew/version5.rst +++ b/docs/source/whatsnew/version5.rst @@ -307,7 +307,7 @@ and tab completions that don't clutter up your history. :target: ../_images/ptshell_features.png These features are provided by the Python library `prompt_toolkit -`__, which replaces +`__, which replaces ``readline`` throughout our terminal interface. Relying on this pure-Python, cross platform module also makes it simpler to diff --git a/docs/source/whatsnew/version7.rst b/docs/source/whatsnew/version7.rst index 308d954..84fccd2 100644 --- a/docs/source/whatsnew/version7.rst +++ b/docs/source/whatsnew/version7.rst @@ -2,6 +2,309 @@ 7.x Series ============ +.. _version 7.28: + +IPython 7.28 +============ + + +IPython 7.28 is again a minor release that mostly bring bugfixes, and couple of +improvement. Many thanks to MrMino, who again did all the work this month, and +made a number of documentation improvements. + +Here is a non-exhaustive list of changes, + +Fixes: + + - async with doesn't allow newlines :ghpull:`13090` + - Dynamically changing to vi mode via %config magic) :ghpull:`13091` + +Virtualenv handling fixes: + + - init_virtualenv now uses Pathlib :ghpull:`12548` + - Fix Improper path comparison of virtualenv directories :ghpull:`13140` + - Fix virtual environment user warning for lower case pathes :ghpull:`13094` + - Adapt to all sorts of drive names for cygwin :ghpull:`13153` + +New Features: + + - enable autoplay in embed YouTube player :ghpull:`13133` + + Documentation: + + - Fix formatting for the core.interactiveshell documentation :ghpull:`13118` + - Fix broken ipyparallel's refs :ghpull:`13138` + - Improve formatting of %time documentation :ghpull:`13125` + - Reword the YouTubeVideo autoplay WN :ghpull:`13147` + + +Thanks +------ + +Many thanks to all the contributors to this release. You can find all individual +contributions to this milestone `on github +`__. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + + +.. _version 7.27: + +IPython 7.27 +============ + +IPython 7.27 is a minor release that fixes a couple of issues and compatibility. + +- Add support for GTK4 :ghpull:`131011` +- Add support for Qt6 :ghpull:`13085` +- Fix an issue with pip magic on windows :ghpull:`13093` + +Thanks +------ + +Many thanks to all the contributors to this release. You can find all individual +contributions to this milestone `on github +`__. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + +.. _version 7.26: + +IPython 7.26 +============ + +IPython 7.26 is a minor release that fixes a couple of issues, updates in API +and Copyright/Licenses issues around various part of the codebase. + +We'll highlight `this issue ` +pointing out we were including and refereeing to code from Stack Overflow which +was CC-BY-SA, hence incompatible with the BSD license of IPython. This lead us +to a rewriting of the corresponding logic which in our case was done in a more +efficient way (in our case we were searching string prefixes instead of full +strings). + +You will notice also a number of documentation improvements and cleanup. + +Of particular interest are the following Pull-requests: + + + - The IPython directive now uses Sphinx logging for warnings. :ghpull:`13030`. + - Add expiry days option to pastebin magic and change http protocol to https. + :ghpull:`13056` + - Make Ipython.utils.timing work with jupyterlite :ghpull:`13050`. + + + +Thanks +------ + +Many thanks to all the contributors to this release and in particular MrMino who +is doing most of the work those days. You can find all individual contributions +to this milestone `on github `__. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + + +.. _version 7.25: + +IPython 7.25 +============ + +IPython 7.25 is a minor release that contains a single bugfix, which is highly +recommended for all users of ipdb, ipython debugger %debug magic and similar. + +Issuing commands like ``where`` from within the debugger would reset the +local variables changes made by the user. It is interesting to look at the root +cause of the issue as accessing an attribute (``frame.f_locals``) would trigger +this side effects. + +Thanks in particular to the patience from the reporters at D.E. Shaw for their +initial bug report that was due to a similar coding oversight in an extension, +and who took time to debug and narrow down the problem. + +Thanks +------ + +Many thanks to all the contributors to this release you can find all individual +contributions to this milestone `on github `__. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries. + + +.. _version 7.24: + +IPython 7.24 +============ + +Third release of IPython for 2021, mostly containing bug fixes. A couple of not +typical updates: + +Misc +---- + + + - Fix an issue where ``%recall`` would both succeeded and print an error message + it failed. :ghpull:`12952` + - Drop support for NumPy 1.16 – practically has no effect beyond indicating in + package metadata that we do not support it. :ghpull:`12937` + +Debugger improvements +--------------------- + +The debugger (and ``%debug`` magic) have been improved and can skip or hide frames +originating from files that are not writable to the user, as these are less +likely to be the source of errors, or be part of system files this can be a useful +addition when debugging long errors. + +In addition to the global ``skip_hidden True|False`` command, the debugger has +gained finer grained control of predicates as to whether to a frame should be +considered hidden. So far 3 predicates are available : + + - ``tbhide``: frames containing the local variable ``__tracebackhide__`` set to + True. + - ``readonly``: frames originating from readonly files, set to False. + - ``ipython_internal``: frames that are likely to be from IPython internal + code, set to True. + +You can toggle individual predicates during a session with + +.. code-block:: + + ipdb> skip_predicates readonly True + +Read-only files will now be considered hidden frames. + + +You can call ``skip_predicates`` without arguments to see the states of current +predicates: + +.. code-block:: + + ipdb> skip_predicates + current predicates: + tbhide : True + readonly : False + ipython_internal : True + +If all predicates are set to ``False``, ``skip_hidden`` will practically have +no effect. We attempt to warn you when all predicates are False. + +Note that the ``readonly`` predicate may increase disk access as we check for +file access permission for all frames on many command invocation, but is usually +cached by operating systems. Let us know if you encounter any issues. + +As the IPython debugger does not use the traitlets infrastructure for +configuration, by editing your ``.pdbrc`` files and appending commands you would +like to be executed just before entering the interactive prompt. For example: + + +.. code:: + + # file : ~/.pdbrc + skip_predicates readonly True + skip_predicates tbhide False + +Will hide read only frames by default and show frames marked with +``__tracebackhide__``. + + + + +Thanks +------ + +Many thanks to all the contributors to this release you can find all individual +contributions to this milestone `on github `__. + +Thanks as well to the `D. E. Shaw group `__ for sponsoring +work on IPython and related libraries, in particular above mentioned +improvements to the debugger. + + + + +.. _version 7.23: + +IPython 7.23 and 7.23.1 +======================= + + +Third release of IPython for 2021, mostly containing bug fixes. A couple of not +typical updates: + + - We moved to GitHub actions away from Travis-CI, the transition may not be + 100% complete (not testing on nightly anymore), but as we ran out of + Travis-Ci hours on the IPython organisation that was a necessary step. + :ghpull:`12900`. + + - We have a new dependency: ``matplotlib-inline``, which try to extract + matplotlib inline backend specific behavior. It is available on PyPI and + conda-forge thus should not be a problem to upgrade to this version. If you + are a package maintainer that might be an extra dependency to package first. + :ghpull:`12817` (IPython 7.23.1 fix a typo that made this change fail) + +In the addition/new feature category, ``display()`` now have a ``clear=True`` +option to clear the display if any further outputs arrives, allowing users to +avoid having to use ``clear_output()`` directly. :ghpull:`12823`. + +In bug fixes category, this release fix an issue when printing tracebacks +containing Unicode characters :ghpull:`12758`. + +In code cleanup category :ghpull:`12932` remove usage of some deprecated +functionality for compatibility with Python 3.10. + + + +Thanks +------ + +Many thanks to all the contributors to this release you can find all individual +contributions to this milestone `on github `__. +In particular MrMino for responding to almost all new issues, and triaging many +of the old ones, as well as takluyver, minrk, willingc for reacting quikly when +we ran out of CI Hours. + +Thanks as well to organisations, QuantStack (martinRenou and SylvainCorlay) for +extracting matplotlib inline backend into its own package, and the `D. E. Shaw group +`__ for sponsoring work on IPython and related libraries. + + +.. _version 7.22: + +IPython 7.22 +============ + +Second release of IPython for 2021, mostly containing bug fixes. Here is a quick +rundown of the few changes. + +- Fix some ``sys.excepthook`` shenanigan when embedding with qt, recommended if + you – for example – use `napari `__. :ghpull:`12842`. +- Fix bug when using the new ipdb ``%context`` magic :ghpull:`12844` +- Couples of deprecation cleanup :ghpull:`12868` +- Update for new dpast.com api if you use the ``%pastbin`` magic. :ghpull:`12712` +- Remove support for numpy before 1.16. :ghpull:`12836` + + +Thanks +------ + +We have a new team member that you should see more often on the IPython +repository, Błażej Michalik (@MrMino) have been doing regular contributions to +IPython, and spent time replying to many issues and guiding new users to the +codebase; they now have triage permissions to the IPython repository and we'll +work toward giving them more permission in the future. + +Many thanks to all the contributors to this release you can find all individual +contributions to this milestone `on github `__. + +Thanks as well to organisations, QuantStack for working on debugger +compatibility for Xeus_python, and the `D. E. Shaw group +`__ for sponsoring work on IPython and related libraries. + .. _version 721: IPython 7.21 @@ -33,7 +336,7 @@ Thanks ------ Many thanks to all the contributors to this release you can find all individual -contribution to this milestone `on github `_. +contribution to this milestone `on github `__. .. _version 720: @@ -82,14 +385,14 @@ was exceptionally no release last month. - Docs docs formatting that make the install commands work on zsh :ghpull:`12587` - Always display the last frame in tracebacks even if hidden with - ``__traceback_hide__`` :ghpull:`12601` + ``__tracebackhide__`` :ghpull:`12601` - Avoid an issue where a callback can be registered multiple times. :ghpull:`12625` - Avoid an issue in debugger mode where frames changes could be lost. :ghpull:`12627` - Never hide the frames that invoke a debugger, even if marked as hidden by - ``__traceback_hide__`` :ghpull:`12631` + ``__tracebackhide__`` :ghpull:`12631` - Fix calling the debugger in a recursive manner :ghpull:`12659` @@ -240,7 +543,7 @@ Change of API and exposed objects automatically detected using `frappuccino `_ (still in beta): -The following items are new and mostly related to understanding ``__tracebackbhide__``:: +The following items are new and mostly related to understanding ``__tracebackbide__``:: + IPython.core.debugger.Pdb.do_down(self, arg) + IPython.core.debugger.Pdb.do_skip_hidden(self, arg) diff --git a/examples/IPython Kernel/Index.ipynb b/examples/IPython Kernel/Index.ipynb index c494bd5..6da3e93 100644 --- a/examples/IPython Kernel/Index.ipynb +++ b/examples/IPython Kernel/Index.ipynb @@ -150,6 +150,7 @@ "  gui-glut.py
\n", "  gui-gtk.py
\n", "  gui-gtk3.py
\n", + "  gui-gtk4.py
\n", "  gui-pyglet.py
\n", "  gui-qt.py
\n", "  gui-tk.py
\n", @@ -160,6 +161,7 @@ " gui-glut.py\n", " gui-gtk.py\n", " gui-gtk3.py\n", + " gui-gtk4.py\n", " gui-pyglet.py\n", " gui-qt.py\n", " gui-tk.py\n", diff --git a/examples/IPython Kernel/Rich Output.ipynb b/examples/IPython Kernel/Rich Output.ipynb index 28c0520..d294daf 100644 --- a/examples/IPython Kernel/Rich Output.ipynb +++ b/examples/IPython Kernel/Rich Output.ipynb @@ -3180,6 +3180,7 @@ "  gui-glut.py
\n", "  gui-gtk.py
\n", "  gui-gtk3.py
\n", + "  gui-gtk4.py
\n", "  gui-pyglet.py
\n", "  gui-qt.py
\n", "  gui-tk.py
\n", @@ -3230,6 +3231,7 @@ " gui-glut.py\n", " gui-gtk.py\n", " gui-gtk3.py\n", + " gui-gtk4.py\n", " gui-pyglet.py\n", " gui-qt.py\n", " gui-tk.py\n", diff --git a/examples/IPython Kernel/gui/gui-gtk4.py b/examples/IPython Kernel/gui/gui-gtk4.py new file mode 100644 index 0000000..bb8c56b --- /dev/null +++ b/examples/IPython Kernel/gui/gui-gtk4.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +"""Simple Gtk example to manually test event loop integration. + +This is meant to run tests manually in ipython as: + +In [1]: %gui gtk4 + +In [2]: %run gui-gtk4.py +""" + +import gi + +gi.require_version("Gtk", "4.0") +from gi.repository import Gtk, GLib # noqa + + +def hello_world(wigdet, data=None): + print("Hello World") + + +def close_request_cb(widget, data=None): + global running + running = False + + +running = True +window = Gtk.Window() +window.connect("close-request", close_request_cb) +button = Gtk.Button(label="Hello World") +button.connect("clicked", hello_world, None) + +window.set_child(button) +window.show() + +context = GLib.MainContext.default() +while running: + context.iteration(True) diff --git a/setup.cfg b/setup.cfg index 0c9e0fc..31a467c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,7 @@ [metadata] license_file = LICENSE + +[velin] +ignore_patterns = + IPython/core/tests, + IPython/testing diff --git a/setup.py b/setup.py index a353555..0696e74 100755 --- a/setup.py +++ b/setup.py @@ -171,27 +171,36 @@ setuptools_extra_args = {} # setuptools requirements extras_require = dict( - parallel = ['ipyparallel'], - qtconsole = ['qtconsole'], - doc = ['Sphinx>=1.3'], - test = ['nose>=0.10.1', 'requests', 'testpath', 'pygments', 'nbformat', 'ipykernel', 'numpy>=1.14'], - terminal = [], - kernel = ['ipykernel'], - nbformat = ['nbformat'], - notebook = ['notebook', 'ipywidgets'], - nbconvert = ['nbconvert'], + parallel=["ipyparallel"], + qtconsole=["qtconsole"], + doc=["Sphinx>=1.3"], + test=[ + "nose>=0.10.1", + "requests", + "testpath", + "pygments", + "nbformat", + "ipykernel", + "numpy>=1.17", + ], + terminal=[], + kernel=["ipykernel"], + nbformat=["nbformat"], + notebook=["notebook", "ipywidgets"], + nbconvert=["nbconvert"], ) install_requires = [ - 'setuptools>=18.5', - 'jedi>=0.16', - 'decorator', - 'pickleshare', - 'traitlets>=4.2', - 'prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1', - 'pygments', - 'backcall', - 'stack_data', + "setuptools>=18.5", + "jedi>=0.16", + "decorator", + "pickleshare", + "traitlets>=4.2", + "prompt_toolkit>=2.0.0,<3.1.0,!=3.0.0,!=3.0.1", + "pygments", + "backcall", + "stack_data", + "matplotlib-inline", ] # Platform-specific dependencies: diff --git a/tools/release_helper.sh b/tools/release_helper.sh index 317ef5e..ee7c06b 100644 --- a/tools/release_helper.sh +++ b/tools/release_helper.sh @@ -21,6 +21,7 @@ WHITE=$(tput setaf 7) NOR=$(tput sgr0) +echo "Will use $EDITOR to edit files when necessary" echo -n "PREV_RELEASE (X.y.z) [$PREV_RELEASE]: " read input PREV_RELEASE=${input:-$PREV_RELEASE} @@ -47,6 +48,18 @@ ask_section(){ } +maybe_edit(){ + echo + echo $BLUE"$1"$NOR + echo -n $GREEN"Press e to Edit $1, any other keys to skip: "$NOR + read -n1 value + echo + if [ $value = 'e' ] ; then + $EDITOR $1 + fi +} + + echo if ask_section "Updating what's new with informations from docs/source/whatsnew/pr" @@ -104,6 +117,7 @@ echo $GREEN"I tried ${RED}sed -i bkp -e '/Uncomment/s/^# //g' IPython/core/relea sed -i bkp -e '/Uncomment/s/^# //g' IPython/core/release.py rm IPython/core/release.pybkp git diff +maybe_edit IPython/core/release.py echo $GREEN"Press enter to continue"$NOR read @@ -151,6 +165,7 @@ then rm IPython/core/release.pybkp git diff echo $GREEN"Please bump ${RED}the minor version number${NOR}" + maybe_edit IPython/core/release.py echo ${BLUE}"Do not commit yet – we'll do it later."$NOR