Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/IPython/__init__.py +144 -0
- lib/python3.12/site-packages/IPython/__main__.py +14 -0
- lib/python3.12/site-packages/IPython/core/compilerop.py +192 -0
- lib/python3.12/site-packages/IPython/core/crashhandler.py +244 -0
- lib/python3.12/site-packages/IPython/core/displaypub.py +205 -0
- lib/python3.12/site-packages/IPython/core/getipython.py +23 -0
- lib/python3.12/site-packages/IPython/core/guarded_eval.py +1656 -0
- lib/python3.12/site-packages/IPython/core/magic.py +786 -0
- lib/python3.12/site-packages/IPython/core/release.py +45 -0
- lib/python3.12/site-packages/IPython/display.py +41 -0
- lib/python3.12/site-packages/IPython/paths.py +122 -0
- lib/python3.12/site-packages/IPython/py.typed +0 -0
- lib/python3.12/site-packages/IPython/sphinxext/__init__.py +0 -0
- lib/python3.12/site-packages/IPython/sphinxext/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/sphinxext/__pycache__/custom_doctests.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/sphinxext/__pycache__/ipython_console_highlighting.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/sphinxext/__pycache__/ipython_directive.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/sphinxext/custom_doctests.py +155 -0
- lib/python3.12/site-packages/IPython/sphinxext/ipython_console_highlighting.py +28 -0
- lib/python3.12/site-packages/IPython/sphinxext/ipython_directive.py +1278 -0
- lib/python3.12/site-packages/IPython/testing/__init__.py +20 -0
- lib/python3.12/site-packages/IPython/testing/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/testing/__pycache__/decorators.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/testing/__pycache__/globalipapp.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/testing/__pycache__/ipunittest.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/testing/__pycache__/skipdoctest.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/testing/__pycache__/tools.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/testing/decorators.py +147 -0
- lib/python3.12/site-packages/IPython/testing/globalipapp.py +114 -0
- lib/python3.12/site-packages/IPython/testing/ipunittest.py +187 -0
- lib/python3.12/site-packages/IPython/testing/plugin/__init__.py +0 -0
- lib/python3.12/site-packages/IPython/testing/plugin/dtexample.py +167 -0
- lib/python3.12/site-packages/IPython/testing/plugin/ipdoctest.py +299 -0
- lib/python3.12/site-packages/IPython/testing/plugin/pytest_ipdoctest.py +877 -0
- lib/python3.12/site-packages/IPython/testing/plugin/setup.py +18 -0
- lib/python3.12/site-packages/IPython/testing/plugin/simple.py +45 -0
- lib/python3.12/site-packages/IPython/testing/plugin/test_combo.txt +36 -0
- lib/python3.12/site-packages/IPython/testing/plugin/test_exampleip.txt +30 -0
- lib/python3.12/site-packages/IPython/testing/plugin/test_ipdoctest.py +92 -0
- lib/python3.12/site-packages/IPython/testing/plugin/test_refs.py +39 -0
- lib/python3.12/site-packages/IPython/testing/skipdoctest.py +19 -0
- lib/python3.12/site-packages/IPython/testing/tools.py +434 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/capture.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/decorators.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/dir2.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/encoding.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/frame.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/generics.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/utils/__pycache__/ipstruct.cpython-312.pyc +0 -0
lib/python3.12/site-packages/IPython/__init__.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PYTHON_ARGCOMPLETE_OK
|
| 2 |
+
"""
|
| 3 |
+
IPython: tools for interactive and parallel computing in Python.
|
| 4 |
+
|
| 5 |
+
https://ipython.org
|
| 6 |
+
"""
|
| 7 |
+
#-----------------------------------------------------------------------------
|
| 8 |
+
# Copyright (c) 2008-2011, IPython Development Team.
|
| 9 |
+
# Copyright (c) 2001-2007, Fernando Perez <fernando.perez@colorado.edu>
|
| 10 |
+
# Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
|
| 11 |
+
# Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
|
| 12 |
+
#
|
| 13 |
+
# Distributed under the terms of the Modified BSD License.
|
| 14 |
+
#
|
| 15 |
+
# The full license is in the file COPYING.txt, distributed with this software.
|
| 16 |
+
#-----------------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
#-----------------------------------------------------------------------------
|
| 19 |
+
# Imports
|
| 20 |
+
#-----------------------------------------------------------------------------
|
| 21 |
+
|
| 22 |
+
import sys
|
| 23 |
+
import warnings
|
| 24 |
+
|
| 25 |
+
#-----------------------------------------------------------------------------
|
| 26 |
+
# Setup everything
|
| 27 |
+
#-----------------------------------------------------------------------------
|
| 28 |
+
|
| 29 |
+
# Don't forget to also update setup.py when this changes!
|
| 30 |
+
if sys.version_info < (3, 11):
|
| 31 |
+
raise ImportError(
|
| 32 |
+
"""
|
| 33 |
+
IPython 8.31+ supports Python 3.11 and above, following SPEC0
|
| 34 |
+
IPython 8.19+ supports Python 3.10 and above, following SPEC0.
|
| 35 |
+
IPython 8.13+ supports Python 3.9 and above, following NEP 29.
|
| 36 |
+
IPython 8.0-8.12 supports Python 3.8 and above, following NEP 29.
|
| 37 |
+
When using Python 2.7, please install IPython 5.x LTS Long Term Support version.
|
| 38 |
+
Python 3.3 and 3.4 were supported up to IPython 6.x.
|
| 39 |
+
Python 3.5 was supported with IPython 7.0 to 7.9.
|
| 40 |
+
Python 3.6 was supported with IPython up to 7.16.
|
| 41 |
+
Python 3.7 was still supported with the 7.x branch.
|
| 42 |
+
|
| 43 |
+
See IPython `README.rst` file for more information:
|
| 44 |
+
|
| 45 |
+
https://github.com/ipython/ipython/blob/main/README.rst
|
| 46 |
+
|
| 47 |
+
"""
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
#-----------------------------------------------------------------------------
|
| 51 |
+
# Setup the top level names
|
| 52 |
+
#-----------------------------------------------------------------------------
|
| 53 |
+
|
| 54 |
+
from .core.getipython import get_ipython
|
| 55 |
+
from .core import release
|
| 56 |
+
from .core.application import Application
|
| 57 |
+
from .terminal.embed import embed
|
| 58 |
+
|
| 59 |
+
from .core.interactiveshell import InteractiveShell
|
| 60 |
+
from .utils.sysinfo import sys_info
|
| 61 |
+
from .utils.frame import extract_module_locals
|
| 62 |
+
|
| 63 |
+
__all__ = ["start_ipython", "embed", "embed_kernel"]
|
| 64 |
+
|
| 65 |
+
# Release data
|
| 66 |
+
__author__ = '%s <%s>' % (release.author, release.author_email)
|
| 67 |
+
__license__ = release.license
|
| 68 |
+
__version__ = release.version
|
| 69 |
+
version_info = release.version_info
|
| 70 |
+
# list of CVEs that should have been patched in this release.
|
| 71 |
+
# this is informational and should not be relied upon.
|
| 72 |
+
__patched_cves__ = {"CVE-2022-21699", "CVE-2023-24816"}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def embed_kernel(module=None, local_ns=None, **kwargs):
|
| 76 |
+
"""Embed and start an IPython kernel in a given scope.
|
| 77 |
+
|
| 78 |
+
If you don't want the kernel to initialize the namespace
|
| 79 |
+
from the scope of the surrounding function,
|
| 80 |
+
and/or you want to load full IPython configuration,
|
| 81 |
+
you probably want `IPython.start_kernel()` instead.
|
| 82 |
+
|
| 83 |
+
This is a deprecated alias for `ipykernel.embed.embed_kernel()`,
|
| 84 |
+
to be removed in the future.
|
| 85 |
+
You should import directly from `ipykernel.embed`; this wrapper
|
| 86 |
+
fails anyway if you don't have `ipykernel` package installed.
|
| 87 |
+
|
| 88 |
+
Parameters
|
| 89 |
+
----------
|
| 90 |
+
module : types.ModuleType, optional
|
| 91 |
+
The module to load into IPython globals (default: caller)
|
| 92 |
+
local_ns : dict, optional
|
| 93 |
+
The namespace to load into IPython user namespace (default: caller)
|
| 94 |
+
**kwargs : various, optional
|
| 95 |
+
Further keyword args are relayed to the IPKernelApp constructor,
|
| 96 |
+
such as `config`, a traitlets :class:`Config` object (see :ref:`configure_start_ipython`),
|
| 97 |
+
allowing configuration of the kernel. Will only have an effect
|
| 98 |
+
on the first embed_kernel call for a given process.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
warnings.warn(
|
| 102 |
+
"import embed_kernel from ipykernel.embed directly (since 2013)."
|
| 103 |
+
" Importing from IPython will be removed in the future",
|
| 104 |
+
DeprecationWarning,
|
| 105 |
+
stacklevel=2,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
(caller_module, caller_locals) = extract_module_locals(1)
|
| 109 |
+
if module is None:
|
| 110 |
+
module = caller_module
|
| 111 |
+
if local_ns is None:
|
| 112 |
+
local_ns = dict(**caller_locals)
|
| 113 |
+
|
| 114 |
+
# Only import .zmq when we really need it
|
| 115 |
+
from ipykernel.embed import embed_kernel as real_embed_kernel
|
| 116 |
+
real_embed_kernel(module=module, local_ns=local_ns, **kwargs)
|
| 117 |
+
|
| 118 |
+
def start_ipython(argv=None, **kwargs):
|
| 119 |
+
"""Launch a normal IPython instance (as opposed to embedded)
|
| 120 |
+
|
| 121 |
+
`IPython.embed()` puts a shell in a particular calling scope,
|
| 122 |
+
such as a function or method for debugging purposes,
|
| 123 |
+
which is often not desirable.
|
| 124 |
+
|
| 125 |
+
`start_ipython()` does full, regular IPython initialization,
|
| 126 |
+
including loading startup files, configuration, etc.
|
| 127 |
+
much of which is skipped by `embed()`.
|
| 128 |
+
|
| 129 |
+
This is a public API method, and will survive implementation changes.
|
| 130 |
+
|
| 131 |
+
Parameters
|
| 132 |
+
----------
|
| 133 |
+
argv : list or None, optional
|
| 134 |
+
If unspecified or None, IPython will parse command-line options from sys.argv.
|
| 135 |
+
To prevent any command-line parsing, pass an empty list: `argv=[]`.
|
| 136 |
+
user_ns : dict, optional
|
| 137 |
+
specify this dictionary to initialize the IPython user namespace with particular values.
|
| 138 |
+
**kwargs : various, optional
|
| 139 |
+
Any other kwargs will be passed to the Application constructor,
|
| 140 |
+
such as `config`, a traitlets :class:`Config` object (see :ref:`configure_start_ipython`),
|
| 141 |
+
allowing configuration of the instance (see :ref:`terminal_options`).
|
| 142 |
+
"""
|
| 143 |
+
from IPython.terminal.ipapp import launch_new_instance
|
| 144 |
+
return launch_new_instance(argv=argv, **kwargs)
|
lib/python3.12/site-packages/IPython/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PYTHON_ARGCOMPLETE_OK
|
| 2 |
+
# encoding: utf-8
|
| 3 |
+
"""Terminal-based IPython entry point."""
|
| 4 |
+
# -----------------------------------------------------------------------------
|
| 5 |
+
# Copyright (c) 2012, IPython Development Team.
|
| 6 |
+
#
|
| 7 |
+
# Distributed under the terms of the Modified BSD License.
|
| 8 |
+
#
|
| 9 |
+
# The full license is in the file COPYING.txt, distributed with this software.
|
| 10 |
+
# -----------------------------------------------------------------------------
|
| 11 |
+
|
| 12 |
+
from IPython import start_ipython
|
| 13 |
+
|
| 14 |
+
start_ipython()
|
lib/python3.12/site-packages/IPython/core/compilerop.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compiler tools with improved interactive support.
|
| 2 |
+
|
| 3 |
+
Provides compilation machinery similar to codeop, but with caching support so
|
| 4 |
+
we can provide interactive tracebacks.
|
| 5 |
+
|
| 6 |
+
Authors
|
| 7 |
+
-------
|
| 8 |
+
* Robert Kern
|
| 9 |
+
* Fernando Perez
|
| 10 |
+
* Thomas Kluyver
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
# Note: though it might be more natural to name this module 'compiler', that
|
| 14 |
+
# name is in the stdlib and name collisions with the stdlib tend to produce
|
| 15 |
+
# weird problems (often with third-party tools).
|
| 16 |
+
|
| 17 |
+
#-----------------------------------------------------------------------------
|
| 18 |
+
# Copyright (C) 2010-2011 The IPython Development Team.
|
| 19 |
+
#
|
| 20 |
+
# Distributed under the terms of the BSD License.
|
| 21 |
+
#
|
| 22 |
+
# The full license is in the file COPYING.txt, distributed with this software.
|
| 23 |
+
#-----------------------------------------------------------------------------
|
| 24 |
+
|
| 25 |
+
#-----------------------------------------------------------------------------
|
| 26 |
+
# Imports
|
| 27 |
+
#-----------------------------------------------------------------------------
|
| 28 |
+
|
| 29 |
+
# Stdlib imports
|
| 30 |
+
import __future__
|
| 31 |
+
from ast import PyCF_ONLY_AST
|
| 32 |
+
import codeop
|
| 33 |
+
import functools
|
| 34 |
+
import hashlib
|
| 35 |
+
import linecache
|
| 36 |
+
import operator
|
| 37 |
+
import time
|
| 38 |
+
from contextlib import contextmanager
|
| 39 |
+
|
| 40 |
+
#-----------------------------------------------------------------------------
|
| 41 |
+
# Constants
|
| 42 |
+
#-----------------------------------------------------------------------------
|
| 43 |
+
|
| 44 |
+
# Roughly equal to PyCF_MASK | PyCF_MASK_OBSOLETE as defined in pythonrun.h,
|
| 45 |
+
# this is used as a bitmask to extract future-related code flags.
|
| 46 |
+
PyCF_MASK = functools.reduce(operator.or_,
|
| 47 |
+
(getattr(__future__, fname).compiler_flag
|
| 48 |
+
for fname in __future__.all_feature_names))
|
| 49 |
+
|
| 50 |
+
#-----------------------------------------------------------------------------
|
| 51 |
+
# Local utilities
|
| 52 |
+
#-----------------------------------------------------------------------------
|
| 53 |
+
|
| 54 |
+
def code_name(code, number=0):
|
| 55 |
+
""" Compute a (probably) unique name for code for caching.
|
| 56 |
+
|
| 57 |
+
This now expects code to be unicode.
|
| 58 |
+
"""
|
| 59 |
+
hash_digest = hashlib.sha1(code.encode("utf-8")).hexdigest()
|
| 60 |
+
# Include the number and 12 characters of the hash in the name. It's
|
| 61 |
+
# pretty much impossible that in a single session we'll have collisions
|
| 62 |
+
# even with truncated hashes, and the full one makes tracebacks too long
|
| 63 |
+
return '<ipython-input-{0}-{1}>'.format(number, hash_digest[:12])
|
| 64 |
+
|
| 65 |
+
#-----------------------------------------------------------------------------
|
| 66 |
+
# Classes and functions
|
| 67 |
+
#-----------------------------------------------------------------------------
|
| 68 |
+
|
| 69 |
+
class CachingCompiler(codeop.Compile):
|
| 70 |
+
"""A compiler that caches code compiled from interactive statements.
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(self):
|
| 74 |
+
codeop.Compile.__init__(self)
|
| 75 |
+
|
| 76 |
+
# Caching a dictionary { filename: execution_count } for nicely
|
| 77 |
+
# rendered tracebacks. The filename corresponds to the filename
|
| 78 |
+
# argument used for the builtins.compile function.
|
| 79 |
+
self._filename_map = {}
|
| 80 |
+
|
| 81 |
+
def ast_parse(self, source, filename='<unknown>', symbol='exec'):
|
| 82 |
+
"""Parse code to an AST with the current compiler flags active.
|
| 83 |
+
|
| 84 |
+
Arguments are exactly the same as ast.parse (in the standard library),
|
| 85 |
+
and are passed to the built-in compile function."""
|
| 86 |
+
return compile(source, filename, symbol, self.flags | PyCF_ONLY_AST, 1)
|
| 87 |
+
|
| 88 |
+
def reset_compiler_flags(self):
|
| 89 |
+
"""Reset compiler flags to default state."""
|
| 90 |
+
# This value is copied from codeop.Compile.__init__, so if that ever
|
| 91 |
+
# changes, it will need to be updated.
|
| 92 |
+
self.flags = codeop.PyCF_DONT_IMPLY_DEDENT
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def compiler_flags(self):
|
| 96 |
+
"""Flags currently active in the compilation process.
|
| 97 |
+
"""
|
| 98 |
+
return self.flags
|
| 99 |
+
|
| 100 |
+
def get_code_name(self, raw_code, transformed_code, number):
|
| 101 |
+
"""Compute filename given the code, and the cell number.
|
| 102 |
+
|
| 103 |
+
Parameters
|
| 104 |
+
----------
|
| 105 |
+
raw_code : str
|
| 106 |
+
The raw cell code.
|
| 107 |
+
transformed_code : str
|
| 108 |
+
The executable Python source code to cache and compile.
|
| 109 |
+
number : int
|
| 110 |
+
A number which forms part of the code's name. Used for the execution
|
| 111 |
+
counter.
|
| 112 |
+
|
| 113 |
+
Returns
|
| 114 |
+
-------
|
| 115 |
+
The computed filename.
|
| 116 |
+
"""
|
| 117 |
+
return code_name(transformed_code, number)
|
| 118 |
+
|
| 119 |
+
def format_code_name(self, name):
|
| 120 |
+
"""Return a user-friendly label and name for a code block.
|
| 121 |
+
|
| 122 |
+
Parameters
|
| 123 |
+
----------
|
| 124 |
+
name : str
|
| 125 |
+
The name for the code block returned from get_code_name
|
| 126 |
+
|
| 127 |
+
Returns
|
| 128 |
+
-------
|
| 129 |
+
A (label, name) pair that can be used in tracebacks, or None if the default formatting should be used.
|
| 130 |
+
"""
|
| 131 |
+
if name in self._filename_map:
|
| 132 |
+
return "Cell", "In[%s]" % self._filename_map[name]
|
| 133 |
+
|
| 134 |
+
def cache(self, transformed_code, number=0, raw_code=None):
|
| 135 |
+
"""Make a name for a block of code, and cache the code.
|
| 136 |
+
|
| 137 |
+
Parameters
|
| 138 |
+
----------
|
| 139 |
+
transformed_code : str
|
| 140 |
+
The executable Python source code to cache and compile.
|
| 141 |
+
number : int
|
| 142 |
+
A number which forms part of the code's name. Used for the execution
|
| 143 |
+
counter.
|
| 144 |
+
raw_code : str
|
| 145 |
+
The raw code before transformation, if None, set to `transformed_code`.
|
| 146 |
+
|
| 147 |
+
Returns
|
| 148 |
+
-------
|
| 149 |
+
The name of the cached code (as a string). Pass this as the filename
|
| 150 |
+
argument to compilation, so that tracebacks are correctly hooked up.
|
| 151 |
+
"""
|
| 152 |
+
if raw_code is None:
|
| 153 |
+
raw_code = transformed_code
|
| 154 |
+
|
| 155 |
+
name = self.get_code_name(raw_code, transformed_code, number)
|
| 156 |
+
|
| 157 |
+
# Save the execution count
|
| 158 |
+
self._filename_map[name] = number
|
| 159 |
+
|
| 160 |
+
# Since Python 2.5, setting mtime to `None` means the lines will
|
| 161 |
+
# never be removed by `linecache.checkcache`. This means all the
|
| 162 |
+
# monkeypatching has *never* been necessary, since this code was
|
| 163 |
+
# only added in 2010, at which point IPython had already stopped
|
| 164 |
+
# supporting Python 2.4.
|
| 165 |
+
#
|
| 166 |
+
# Note that `linecache.clearcache` and `linecache.updatecache` may
|
| 167 |
+
# still remove our code from the cache, but those show explicit
|
| 168 |
+
# intent, and we should not try to interfere. Normally the former
|
| 169 |
+
# is never called except when out of memory, and the latter is only
|
| 170 |
+
# called for lines *not* in the cache.
|
| 171 |
+
entry = (
|
| 172 |
+
len(transformed_code),
|
| 173 |
+
None,
|
| 174 |
+
[line + "\n" for line in transformed_code.splitlines()],
|
| 175 |
+
name,
|
| 176 |
+
)
|
| 177 |
+
linecache.cache[name] = entry
|
| 178 |
+
return name
|
| 179 |
+
|
| 180 |
+
@contextmanager
|
| 181 |
+
def extra_flags(self, flags):
|
| 182 |
+
## bits that we'll set to 1
|
| 183 |
+
turn_on_bits = ~self.flags & flags
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
self.flags = self.flags | flags
|
| 187 |
+
try:
|
| 188 |
+
yield
|
| 189 |
+
finally:
|
| 190 |
+
# turn off only the bits we turned on so that something like
|
| 191 |
+
# __future__ that set flags stays.
|
| 192 |
+
self.flags &= ~turn_on_bits
|
lib/python3.12/site-packages/IPython/core/crashhandler.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""sys.excepthook for IPython itself, leaves a detailed report on disk.
|
| 2 |
+
|
| 3 |
+
Authors:
|
| 4 |
+
|
| 5 |
+
* Fernando Perez
|
| 6 |
+
* Brian E. Granger
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
#-----------------------------------------------------------------------------
|
| 10 |
+
# Copyright (C) 2001-2007 Fernando Perez. <fperez@colorado.edu>
|
| 11 |
+
# Copyright (C) 2008-2011 The IPython Development Team
|
| 12 |
+
#
|
| 13 |
+
# Distributed under the terms of the BSD License. The full license is in
|
| 14 |
+
# the file COPYING, distributed as part of this software.
|
| 15 |
+
#-----------------------------------------------------------------------------
|
| 16 |
+
|
| 17 |
+
#-----------------------------------------------------------------------------
|
| 18 |
+
# Imports
|
| 19 |
+
#-----------------------------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
import sys
|
| 22 |
+
import traceback
|
| 23 |
+
from pprint import pformat
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
import builtins as builtin_mod
|
| 27 |
+
|
| 28 |
+
from IPython.core import ultratb
|
| 29 |
+
from IPython.core.application import Application
|
| 30 |
+
from IPython.core.release import author_email
|
| 31 |
+
from IPython.utils.sysinfo import sys_info
|
| 32 |
+
|
| 33 |
+
from IPython.core.release import __version__ as version
|
| 34 |
+
|
| 35 |
+
from typing import Optional, Dict
|
| 36 |
+
import types
|
| 37 |
+
|
| 38 |
+
#-----------------------------------------------------------------------------
|
| 39 |
+
# Code
|
| 40 |
+
#-----------------------------------------------------------------------------
|
| 41 |
+
|
| 42 |
+
# Template for the user message.
|
| 43 |
+
_default_message_template = """\
|
| 44 |
+
Oops, {app_name} crashed. We do our best to make it stable, but...
|
| 45 |
+
|
| 46 |
+
A crash report was automatically generated with the following information:
|
| 47 |
+
- A verbatim copy of the crash traceback.
|
| 48 |
+
- A copy of your input history during this session.
|
| 49 |
+
- Data on your current {app_name} configuration.
|
| 50 |
+
|
| 51 |
+
It was left in the file named:
|
| 52 |
+
\t'{crash_report_fname}'
|
| 53 |
+
If you can email this file to the developers, the information in it will help
|
| 54 |
+
them in understanding and correcting the problem.
|
| 55 |
+
|
| 56 |
+
You can mail it to: {contact_name} at {contact_email}
|
| 57 |
+
with the subject '{app_name} Crash Report'.
|
| 58 |
+
|
| 59 |
+
If you want to do it now, the following command will work (under Unix):
|
| 60 |
+
mail -s '{app_name} Crash Report' {contact_email} < {crash_report_fname}
|
| 61 |
+
|
| 62 |
+
In your email, please also include information about:
|
| 63 |
+
- The operating system under which the crash happened: Linux, macOS, Windows,
|
| 64 |
+
other, and which exact version (for example: Ubuntu 16.04.3, macOS 10.13.2,
|
| 65 |
+
Windows 10 Pro), and whether it is 32-bit or 64-bit;
|
| 66 |
+
- How {app_name} was installed: using pip or conda, from GitHub, as part of
|
| 67 |
+
a Docker container, or other, providing more detail if possible;
|
| 68 |
+
- How to reproduce the crash: what exact sequence of instructions can one
|
| 69 |
+
input to get the same crash? Ideally, find a minimal yet complete sequence
|
| 70 |
+
of instructions that yields the crash.
|
| 71 |
+
|
| 72 |
+
To ensure accurate tracking of this issue, please file a report about it at:
|
| 73 |
+
{bug_tracker}
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
_lite_message_template = """
|
| 77 |
+
If you suspect this is an IPython {version} bug, please report it at:
|
| 78 |
+
https://github.com/ipython/ipython/issues
|
| 79 |
+
or send an email to the mailing list at {email}
|
| 80 |
+
|
| 81 |
+
You can print a more detailed traceback right now with "%tb", or use "%debug"
|
| 82 |
+
to interactively debug it.
|
| 83 |
+
|
| 84 |
+
Extra-detailed tracebacks for bug-reporting purposes can be enabled via:
|
| 85 |
+
{config}Application.verbose_crash=True
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class CrashHandler:
|
| 90 |
+
"""Customizable crash handlers for IPython applications.
|
| 91 |
+
|
| 92 |
+
Instances of this class provide a :meth:`__call__` method which can be
|
| 93 |
+
used as a ``sys.excepthook``. The :meth:`__call__` signature is::
|
| 94 |
+
|
| 95 |
+
def __call__(self, etype, evalue, etb)
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
message_template = _default_message_template
|
| 99 |
+
section_sep = '\n\n'+'*'*75+'\n\n'
|
| 100 |
+
info: Dict[str, Optional[str]]
|
| 101 |
+
|
| 102 |
+
def __init__(
|
| 103 |
+
self,
|
| 104 |
+
app: Application,
|
| 105 |
+
contact_name: Optional[str] = None,
|
| 106 |
+
contact_email: Optional[str] = None,
|
| 107 |
+
bug_tracker: Optional[str] = None,
|
| 108 |
+
show_crash_traceback: bool = True,
|
| 109 |
+
call_pdb: bool = False,
|
| 110 |
+
):
|
| 111 |
+
"""Create a new crash handler
|
| 112 |
+
|
| 113 |
+
Parameters
|
| 114 |
+
----------
|
| 115 |
+
app : Application
|
| 116 |
+
A running :class:`Application` instance, which will be queried at
|
| 117 |
+
crash time for internal information.
|
| 118 |
+
contact_name : str
|
| 119 |
+
A string with the name of the person to contact.
|
| 120 |
+
contact_email : str
|
| 121 |
+
A string with the email address of the contact.
|
| 122 |
+
bug_tracker : str
|
| 123 |
+
A string with the URL for your project's bug tracker.
|
| 124 |
+
show_crash_traceback : bool
|
| 125 |
+
If false, don't print the crash traceback on stderr, only generate
|
| 126 |
+
the on-disk report
|
| 127 |
+
call_pdb
|
| 128 |
+
Whether to call pdb on crash
|
| 129 |
+
|
| 130 |
+
Attributes
|
| 131 |
+
----------
|
| 132 |
+
These instances contain some non-argument attributes which allow for
|
| 133 |
+
further customization of the crash handler's behavior. Please see the
|
| 134 |
+
source for further details.
|
| 135 |
+
|
| 136 |
+
"""
|
| 137 |
+
self.crash_report_fname = "Crash_report_%s.txt" % app.name
|
| 138 |
+
self.app = app
|
| 139 |
+
self.call_pdb = call_pdb
|
| 140 |
+
#self.call_pdb = True # dbg
|
| 141 |
+
self.show_crash_traceback = show_crash_traceback
|
| 142 |
+
self.info = dict(app_name = app.name,
|
| 143 |
+
contact_name = contact_name,
|
| 144 |
+
contact_email = contact_email,
|
| 145 |
+
bug_tracker = bug_tracker,
|
| 146 |
+
crash_report_fname = self.crash_report_fname)
|
| 147 |
+
|
| 148 |
+
def __call__(
|
| 149 |
+
self,
|
| 150 |
+
etype: type[BaseException],
|
| 151 |
+
evalue: BaseException,
|
| 152 |
+
etb: types.TracebackType,
|
| 153 |
+
) -> None:
|
| 154 |
+
"""Handle an exception, call for compatible with sys.excepthook"""
|
| 155 |
+
|
| 156 |
+
# do not allow the crash handler to be called twice without reinstalling it
|
| 157 |
+
# this prevents unlikely errors in the crash handling from entering an
|
| 158 |
+
# infinite loop.
|
| 159 |
+
sys.excepthook = sys.__excepthook__
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# Use this ONLY for developer debugging (keep commented out for release)
|
| 163 |
+
ipython_dir = getattr(self.app, "ipython_dir", None)
|
| 164 |
+
if ipython_dir is not None:
|
| 165 |
+
assert isinstance(ipython_dir, str)
|
| 166 |
+
rptdir = Path(ipython_dir)
|
| 167 |
+
else:
|
| 168 |
+
rptdir = Path.cwd()
|
| 169 |
+
if not rptdir.is_dir():
|
| 170 |
+
rptdir = Path.cwd()
|
| 171 |
+
report_name = rptdir / self.crash_report_fname
|
| 172 |
+
# write the report filename into the instance dict so it can get
|
| 173 |
+
# properly expanded out in the user message template
|
| 174 |
+
self.crash_report_fname = str(report_name)
|
| 175 |
+
self.info["crash_report_fname"] = str(report_name)
|
| 176 |
+
TBhandler = ultratb.VerboseTB(
|
| 177 |
+
theme_name="nocolor",
|
| 178 |
+
long_header=True,
|
| 179 |
+
call_pdb=self.call_pdb,
|
| 180 |
+
)
|
| 181 |
+
if self.call_pdb:
|
| 182 |
+
TBhandler(etype,evalue,etb)
|
| 183 |
+
return
|
| 184 |
+
else:
|
| 185 |
+
traceback = TBhandler.text(etype,evalue,etb,context=31)
|
| 186 |
+
|
| 187 |
+
# print traceback to screen
|
| 188 |
+
if self.show_crash_traceback:
|
| 189 |
+
print(traceback, file=sys.stderr)
|
| 190 |
+
|
| 191 |
+
# and generate a complete report on disk
|
| 192 |
+
try:
|
| 193 |
+
report = open(report_name, "w", encoding="utf-8")
|
| 194 |
+
except:
|
| 195 |
+
print('Could not create crash report on disk.', file=sys.stderr)
|
| 196 |
+
return
|
| 197 |
+
|
| 198 |
+
with report:
|
| 199 |
+
# Inform user on stderr of what happened
|
| 200 |
+
print('\n'+'*'*70+'\n', file=sys.stderr)
|
| 201 |
+
print(self.message_template.format(**self.info), file=sys.stderr)
|
| 202 |
+
|
| 203 |
+
# Construct report on disk
|
| 204 |
+
report.write(self.make_report(str(traceback)))
|
| 205 |
+
|
| 206 |
+
builtin_mod.input("Hit <Enter> to quit (your terminal may close):")
|
| 207 |
+
|
| 208 |
+
def make_report(self, traceback: str) -> str:
|
| 209 |
+
"""Return a string containing a crash report."""
|
| 210 |
+
|
| 211 |
+
sec_sep = self.section_sep
|
| 212 |
+
|
| 213 |
+
report = ['*'*75+'\n\n'+'IPython post-mortem report\n\n']
|
| 214 |
+
rpt_add = report.append
|
| 215 |
+
rpt_add(sys_info())
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
config = pformat(self.app.config)
|
| 219 |
+
rpt_add(sec_sep)
|
| 220 |
+
rpt_add("Application name: %s\n\n" % self.app.name)
|
| 221 |
+
rpt_add("Current user configuration structure:\n\n")
|
| 222 |
+
rpt_add(config)
|
| 223 |
+
except:
|
| 224 |
+
pass
|
| 225 |
+
rpt_add(sec_sep+'Crash traceback:\n\n' + traceback)
|
| 226 |
+
|
| 227 |
+
return ''.join(report)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def crash_handler_lite(
|
| 231 |
+
etype: type[BaseException], evalue: BaseException, tb: types.TracebackType
|
| 232 |
+
) -> None:
|
| 233 |
+
"""a light excepthook, adding a small message to the usual traceback"""
|
| 234 |
+
traceback.print_exception(etype, evalue, tb)
|
| 235 |
+
|
| 236 |
+
from IPython.core.interactiveshell import InteractiveShell
|
| 237 |
+
if InteractiveShell.initialized():
|
| 238 |
+
# we are in a Shell environment, give %magic example
|
| 239 |
+
config = "%config "
|
| 240 |
+
else:
|
| 241 |
+
# we are not in a shell, show generic config
|
| 242 |
+
config = "c."
|
| 243 |
+
print(_lite_message_template.format(email=author_email, config=config, version=version), file=sys.stderr)
|
| 244 |
+
|
lib/python3.12/site-packages/IPython/core/displaypub.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""An interface for publishing rich data to frontends.
|
| 2 |
+
|
| 3 |
+
There are two components of the display system:
|
| 4 |
+
|
| 5 |
+
* Display formatters, which take a Python object and compute the
|
| 6 |
+
representation of the object in various formats (text, HTML, SVG, etc.).
|
| 7 |
+
* The display publisher that is used to send the representation data to the
|
| 8 |
+
various frontends.
|
| 9 |
+
|
| 10 |
+
This module defines the logic display publishing. The display publisher uses
|
| 11 |
+
the ``display_data`` message type that is defined in the IPython messaging
|
| 12 |
+
spec.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
# Copyright (c) IPython Development Team.
|
| 16 |
+
# Distributed under the terms of the Modified BSD License.
|
| 17 |
+
|
| 18 |
+
import sys
|
| 19 |
+
|
| 20 |
+
from traitlets.config.configurable import Configurable
|
| 21 |
+
from traitlets import List
|
| 22 |
+
|
| 23 |
+
# This used to be defined here - it is imported for backwards compatibility
|
| 24 |
+
from .display_functions import publish_display_data
|
| 25 |
+
from .history import HistoryOutput
|
| 26 |
+
|
| 27 |
+
import typing as t
|
| 28 |
+
|
| 29 |
+
# -----------------------------------------------------------------------------
|
| 30 |
+
# Main payload class
|
| 31 |
+
# -----------------------------------------------------------------------------
|
| 32 |
+
|
| 33 |
+
_sentinel = object()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class DisplayPublisher(Configurable):
|
| 37 |
+
"""A traited class that publishes display data to frontends.
|
| 38 |
+
|
| 39 |
+
Instances of this class are created by the main IPython object and should
|
| 40 |
+
be accessed there.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(self, shell=None, *args, **kwargs):
|
| 44 |
+
self.shell = shell
|
| 45 |
+
self._is_publishing = False
|
| 46 |
+
self._in_post_execute = False
|
| 47 |
+
if self.shell:
|
| 48 |
+
self._setup_execution_tracking()
|
| 49 |
+
super().__init__(*args, **kwargs)
|
| 50 |
+
|
| 51 |
+
def _validate_data(self, data, metadata=None):
|
| 52 |
+
"""Validate the display data.
|
| 53 |
+
|
| 54 |
+
Parameters
|
| 55 |
+
----------
|
| 56 |
+
data : dict
|
| 57 |
+
The formata data dictionary.
|
| 58 |
+
metadata : dict
|
| 59 |
+
Any metadata for the data.
|
| 60 |
+
"""
|
| 61 |
+
|
| 62 |
+
if not isinstance(data, dict):
|
| 63 |
+
raise TypeError("data must be a dict, got: %r" % data)
|
| 64 |
+
if metadata is not None:
|
| 65 |
+
if not isinstance(metadata, dict):
|
| 66 |
+
raise TypeError("metadata must be a dict, got: %r" % data)
|
| 67 |
+
|
| 68 |
+
def _setup_execution_tracking(self):
|
| 69 |
+
"""Set up hooks to track execution state"""
|
| 70 |
+
self.shell.events.register("post_execute", self._on_post_execute)
|
| 71 |
+
self.shell.events.register("pre_execute", self._on_pre_execute)
|
| 72 |
+
|
| 73 |
+
def _on_post_execute(self):
|
| 74 |
+
"""Called at start of post_execute phase"""
|
| 75 |
+
self._in_post_execute = True
|
| 76 |
+
|
| 77 |
+
def _on_pre_execute(self):
|
| 78 |
+
"""Called at start of pre_execute phase"""
|
| 79 |
+
self._in_post_execute = False
|
| 80 |
+
|
| 81 |
+
# use * to indicate transient, update are keyword-only
|
| 82 |
+
def publish(
|
| 83 |
+
self,
|
| 84 |
+
data,
|
| 85 |
+
metadata=None,
|
| 86 |
+
source=_sentinel,
|
| 87 |
+
*,
|
| 88 |
+
transient=None,
|
| 89 |
+
update=False,
|
| 90 |
+
**kwargs,
|
| 91 |
+
) -> None:
|
| 92 |
+
"""Publish data and metadata to all frontends.
|
| 93 |
+
|
| 94 |
+
See the ``display_data`` message in the messaging documentation for
|
| 95 |
+
more details about this message type.
|
| 96 |
+
|
| 97 |
+
The following MIME types are currently implemented:
|
| 98 |
+
|
| 99 |
+
* text/plain
|
| 100 |
+
* text/html
|
| 101 |
+
* text/markdown
|
| 102 |
+
* text/latex
|
| 103 |
+
* application/json
|
| 104 |
+
* application/javascript
|
| 105 |
+
* image/png
|
| 106 |
+
* image/jpeg
|
| 107 |
+
* image/svg+xml
|
| 108 |
+
|
| 109 |
+
Parameters
|
| 110 |
+
----------
|
| 111 |
+
data : dict
|
| 112 |
+
A dictionary having keys that are valid MIME types (like
|
| 113 |
+
'text/plain' or 'image/svg+xml') and values that are the data for
|
| 114 |
+
that MIME type. The data itself must be a JSON'able data
|
| 115 |
+
structure. Minimally all data should have the 'text/plain' data,
|
| 116 |
+
which can be displayed by all frontends. If more than the plain
|
| 117 |
+
text is given, it is up to the frontend to decide which
|
| 118 |
+
representation to use.
|
| 119 |
+
metadata : dict
|
| 120 |
+
A dictionary for metadata related to the data. This can contain
|
| 121 |
+
arbitrary key, value pairs that frontends can use to interpret
|
| 122 |
+
the data. Metadata specific to each mime-type can be specified
|
| 123 |
+
in the metadata dict with the same mime-type keys as
|
| 124 |
+
the data itself.
|
| 125 |
+
source : str, deprecated
|
| 126 |
+
Unused.
|
| 127 |
+
transient : dict, keyword-only
|
| 128 |
+
A dictionary for transient data.
|
| 129 |
+
Data in this dictionary should not be persisted as part of saving this output.
|
| 130 |
+
Examples include 'display_id'.
|
| 131 |
+
update : bool, keyword-only, default: False
|
| 132 |
+
If True, only update existing outputs with the same display_id,
|
| 133 |
+
rather than creating a new output.
|
| 134 |
+
"""
|
| 135 |
+
|
| 136 |
+
if source is not _sentinel:
|
| 137 |
+
import warnings
|
| 138 |
+
|
| 139 |
+
warnings.warn(
|
| 140 |
+
"The 'source' parameter is deprecated since IPython 3.0 and will be ignored "
|
| 141 |
+
"(this warning is present since 9.0). `source` parameter will be removed in the future.",
|
| 142 |
+
DeprecationWarning,
|
| 143 |
+
stacklevel=2,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
handlers: t.Dict = {}
|
| 147 |
+
if self.shell is not None:
|
| 148 |
+
handlers = getattr(self.shell, "mime_renderers", {})
|
| 149 |
+
|
| 150 |
+
outputs = self.shell.history_manager.outputs
|
| 151 |
+
|
| 152 |
+
target_execution_count = self.shell.execution_count - 1
|
| 153 |
+
if self._in_post_execute:
|
| 154 |
+
# We're in post_execute, so this is likely a matplotlib flush
|
| 155 |
+
# Use execution_count - 1 to associate with the cell that created the plot
|
| 156 |
+
target_execution_count = self.shell.execution_count - 1
|
| 157 |
+
|
| 158 |
+
outputs[target_execution_count].append(
|
| 159 |
+
HistoryOutput(output_type="display_data", bundle=data)
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
for mime, handler in handlers.items():
|
| 163 |
+
if mime in data:
|
| 164 |
+
handler(data[mime], metadata.get(mime, None))
|
| 165 |
+
return
|
| 166 |
+
|
| 167 |
+
self._is_publishing = True
|
| 168 |
+
if "text/plain" in data:
|
| 169 |
+
print(data["text/plain"])
|
| 170 |
+
self._is_publishing = False
|
| 171 |
+
|
| 172 |
+
@property
|
| 173 |
+
def is_publishing(self):
|
| 174 |
+
return self._is_publishing
|
| 175 |
+
|
| 176 |
+
def clear_output(self, wait=False):
|
| 177 |
+
"""Clear the output of the cell receiving output."""
|
| 178 |
+
print("\033[2K\r", end="")
|
| 179 |
+
sys.stdout.flush()
|
| 180 |
+
print("\033[2K\r", end="")
|
| 181 |
+
sys.stderr.flush()
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
class CapturingDisplayPublisher(DisplayPublisher):
|
| 185 |
+
"""A DisplayPublisher that stores"""
|
| 186 |
+
|
| 187 |
+
outputs: List = List()
|
| 188 |
+
|
| 189 |
+
def publish(
|
| 190 |
+
self, data, metadata=None, source=None, *, transient=None, update=False
|
| 191 |
+
):
|
| 192 |
+
self.outputs.append(
|
| 193 |
+
{
|
| 194 |
+
"data": data,
|
| 195 |
+
"metadata": metadata,
|
| 196 |
+
"transient": transient,
|
| 197 |
+
"update": update,
|
| 198 |
+
}
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
def clear_output(self, wait=False):
|
| 202 |
+
super(CapturingDisplayPublisher, self).clear_output(wait)
|
| 203 |
+
|
| 204 |
+
# empty the list, *do not* reassign a new list
|
| 205 |
+
self.outputs.clear()
|
lib/python3.12/site-packages/IPython/core/getipython.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Simple function to call to get the current InteractiveShell instance
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
#-----------------------------------------------------------------------------
|
| 5 |
+
# Copyright (C) 2013 The IPython Development Team
|
| 6 |
+
#
|
| 7 |
+
# Distributed under the terms of the BSD License. The full license is in
|
| 8 |
+
# the file COPYING, distributed as part of this software.
|
| 9 |
+
#-----------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
#-----------------------------------------------------------------------------
|
| 12 |
+
# Classes and functions
|
| 13 |
+
#-----------------------------------------------------------------------------
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def get_ipython():
|
| 17 |
+
"""Get the global InteractiveShell instance.
|
| 18 |
+
|
| 19 |
+
Returns None if no InteractiveShell instance is registered.
|
| 20 |
+
"""
|
| 21 |
+
from IPython.core.interactiveshell import InteractiveShell
|
| 22 |
+
if InteractiveShell.initialized():
|
| 23 |
+
return InteractiveShell.instance()
|
lib/python3.12/site-packages/IPython/core/guarded_eval.py
ADDED
|
@@ -0,0 +1,1656 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from copy import copy
|
| 2 |
+
from inspect import isclass, signature, Signature, getmodule
|
| 3 |
+
from typing import (
|
| 4 |
+
Annotated,
|
| 5 |
+
AnyStr,
|
| 6 |
+
Literal,
|
| 7 |
+
NamedTuple,
|
| 8 |
+
NewType,
|
| 9 |
+
Optional,
|
| 10 |
+
Protocol,
|
| 11 |
+
TypeGuard,
|
| 12 |
+
Union,
|
| 13 |
+
get_args,
|
| 14 |
+
get_origin,
|
| 15 |
+
is_typeddict,
|
| 16 |
+
)
|
| 17 |
+
from collections.abc import Callable, Sequence
|
| 18 |
+
import ast
|
| 19 |
+
import builtins
|
| 20 |
+
import collections
|
| 21 |
+
import dataclasses
|
| 22 |
+
import operator
|
| 23 |
+
import sys
|
| 24 |
+
import typing
|
| 25 |
+
import warnings
|
| 26 |
+
from functools import cached_property
|
| 27 |
+
from dataclasses import dataclass, field
|
| 28 |
+
from types import MethodDescriptorType, ModuleType, MethodType
|
| 29 |
+
|
| 30 |
+
from IPython.utils.decorators import undoc
|
| 31 |
+
|
| 32 |
+
import types
|
| 33 |
+
from typing import Self, LiteralString, get_type_hints
|
| 34 |
+
|
| 35 |
+
if sys.version_info < (3, 12):
|
| 36 |
+
from typing_extensions import TypeAliasType
|
| 37 |
+
else:
|
| 38 |
+
from typing import TypeAliasType
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@undoc
|
| 42 |
+
class HasGetItem(Protocol):
|
| 43 |
+
def __getitem__(self, key) -> None:
|
| 44 |
+
...
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@undoc
|
| 48 |
+
class InstancesHaveGetItem(Protocol):
|
| 49 |
+
def __call__(self, *args, **kwargs) -> HasGetItem:
|
| 50 |
+
...
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@undoc
|
| 54 |
+
class HasGetAttr(Protocol):
|
| 55 |
+
def __getattr__(self, key) -> None:
|
| 56 |
+
...
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@undoc
|
| 60 |
+
class DoesNotHaveGetAttr(Protocol):
|
| 61 |
+
pass
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# By default `__getattr__` is not explicitly implemented on most objects
|
| 65 |
+
MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _unbind_method(func: Callable) -> Union[Callable, None]:
|
| 69 |
+
"""Get unbound method for given bound method.
|
| 70 |
+
|
| 71 |
+
Returns None if cannot get unbound method, or method is already unbound.
|
| 72 |
+
"""
|
| 73 |
+
owner = getattr(func, "__self__", None)
|
| 74 |
+
owner_class = type(owner)
|
| 75 |
+
name = getattr(func, "__name__", None)
|
| 76 |
+
instance_dict_overrides = getattr(owner, "__dict__", None)
|
| 77 |
+
if (
|
| 78 |
+
owner is not None
|
| 79 |
+
and name
|
| 80 |
+
and (
|
| 81 |
+
not instance_dict_overrides
|
| 82 |
+
or (instance_dict_overrides and name not in instance_dict_overrides)
|
| 83 |
+
)
|
| 84 |
+
):
|
| 85 |
+
return getattr(owner_class, name)
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
@undoc
|
| 90 |
+
@dataclass
|
| 91 |
+
class EvaluationPolicy:
|
| 92 |
+
"""Definition of evaluation policy."""
|
| 93 |
+
|
| 94 |
+
allow_locals_access: bool = False
|
| 95 |
+
allow_globals_access: bool = False
|
| 96 |
+
allow_item_access: bool = False
|
| 97 |
+
allow_attr_access: bool = False
|
| 98 |
+
allow_builtins_access: bool = False
|
| 99 |
+
allow_all_operations: bool = False
|
| 100 |
+
allow_any_calls: bool = False
|
| 101 |
+
allow_auto_import: bool = False
|
| 102 |
+
allowed_calls: set[Callable] = field(default_factory=set)
|
| 103 |
+
|
| 104 |
+
def can_get_item(self, value, item):
|
| 105 |
+
return self.allow_item_access
|
| 106 |
+
|
| 107 |
+
def can_get_attr(self, value, attr):
|
| 108 |
+
return self.allow_attr_access
|
| 109 |
+
|
| 110 |
+
def can_operate(self, dunders: tuple[str, ...], a, b=None):
|
| 111 |
+
if self.allow_all_operations:
|
| 112 |
+
return True
|
| 113 |
+
|
| 114 |
+
def can_call(self, func):
|
| 115 |
+
if self.allow_any_calls:
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
if func in self.allowed_calls:
|
| 119 |
+
return True
|
| 120 |
+
|
| 121 |
+
owner_method = _unbind_method(func)
|
| 122 |
+
|
| 123 |
+
if owner_method and owner_method in self.allowed_calls:
|
| 124 |
+
return True
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _get_external(module_name: str, access_path: Sequence[str]):
|
| 128 |
+
"""Get value from external module given a dotted access path.
|
| 129 |
+
|
| 130 |
+
Only gets value if the module is already imported.
|
| 131 |
+
|
| 132 |
+
Raises:
|
| 133 |
+
* `KeyError` if module is removed not found, and
|
| 134 |
+
* `AttributeError` if access path does not match an exported object
|
| 135 |
+
"""
|
| 136 |
+
try:
|
| 137 |
+
member_type = sys.modules[module_name]
|
| 138 |
+
# standard module
|
| 139 |
+
for attr in access_path:
|
| 140 |
+
member_type = getattr(member_type, attr)
|
| 141 |
+
return member_type
|
| 142 |
+
except (KeyError, AttributeError):
|
| 143 |
+
# handle modules in namespace packages
|
| 144 |
+
module_path = ".".join([module_name, *access_path])
|
| 145 |
+
if module_path in sys.modules:
|
| 146 |
+
return sys.modules[module_path]
|
| 147 |
+
raise
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def _has_original_dunder_external(
|
| 151 |
+
value,
|
| 152 |
+
module_name: str,
|
| 153 |
+
access_path: Sequence[str],
|
| 154 |
+
method_name: str,
|
| 155 |
+
):
|
| 156 |
+
if module_name not in sys.modules:
|
| 157 |
+
full_module_path = ".".join([module_name, *access_path])
|
| 158 |
+
if full_module_path not in sys.modules:
|
| 159 |
+
# LBYLB as it is faster
|
| 160 |
+
return False
|
| 161 |
+
try:
|
| 162 |
+
member_type = _get_external(module_name, access_path)
|
| 163 |
+
value_type = type(value)
|
| 164 |
+
if type(value) == member_type:
|
| 165 |
+
return True
|
| 166 |
+
if isinstance(member_type, ModuleType):
|
| 167 |
+
value_module = getmodule(value_type)
|
| 168 |
+
if not value_module or not value_module.__name__:
|
| 169 |
+
return False
|
| 170 |
+
if (
|
| 171 |
+
value_module.__name__ == member_type.__name__
|
| 172 |
+
or value_module.__name__.startswith(member_type.__name__ + ".")
|
| 173 |
+
):
|
| 174 |
+
return True
|
| 175 |
+
if method_name == "__getattribute__":
|
| 176 |
+
# we have to short-circuit here due to an unresolved issue in
|
| 177 |
+
# `isinstance` implementation: https://bugs.python.org/issue32683
|
| 178 |
+
return False
|
| 179 |
+
if not isinstance(member_type, ModuleType) and isinstance(value, member_type):
|
| 180 |
+
method = getattr(value_type, method_name, None)
|
| 181 |
+
member_method = getattr(member_type, method_name, None)
|
| 182 |
+
if member_method == method:
|
| 183 |
+
return True
|
| 184 |
+
if isinstance(member_type, ModuleType):
|
| 185 |
+
method = getattr(value_type, method_name, None)
|
| 186 |
+
for base_class in value_type.__mro__[1:]:
|
| 187 |
+
base_module = getmodule(base_class)
|
| 188 |
+
if base_module and (
|
| 189 |
+
base_module.__name__ == member_type.__name__
|
| 190 |
+
or base_module.__name__.startswith(member_type.__name__ + ".")
|
| 191 |
+
):
|
| 192 |
+
# Check if the method comes from this trusted base class
|
| 193 |
+
base_method = getattr(base_class, method_name, None)
|
| 194 |
+
if base_method is not None and base_method == method:
|
| 195 |
+
return True
|
| 196 |
+
except (AttributeError, KeyError):
|
| 197 |
+
return False
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _has_original_dunder(
|
| 201 |
+
value, allowed_types, allowed_methods, allowed_external, method_name
|
| 202 |
+
):
|
| 203 |
+
# note: Python ignores `__getattr__`/`__getitem__` on instances,
|
| 204 |
+
# we only need to check at class level
|
| 205 |
+
value_type = type(value)
|
| 206 |
+
|
| 207 |
+
# strict type check passes → no need to check method
|
| 208 |
+
if value_type in allowed_types:
|
| 209 |
+
return True
|
| 210 |
+
|
| 211 |
+
method = getattr(value_type, method_name, None)
|
| 212 |
+
|
| 213 |
+
if method is None:
|
| 214 |
+
return None
|
| 215 |
+
|
| 216 |
+
if method in allowed_methods:
|
| 217 |
+
return True
|
| 218 |
+
|
| 219 |
+
for module_name, *access_path in allowed_external:
|
| 220 |
+
if _has_original_dunder_external(value, module_name, access_path, method_name):
|
| 221 |
+
return True
|
| 222 |
+
|
| 223 |
+
return False
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def _coerce_path_to_tuples(
|
| 227 |
+
allow_list: set[tuple[str, ...] | str],
|
| 228 |
+
) -> set[tuple[str, ...]]:
|
| 229 |
+
"""Replace dotted paths on the provided allow-list with tuples."""
|
| 230 |
+
return {
|
| 231 |
+
path if isinstance(path, tuple) else tuple(path.split("."))
|
| 232 |
+
for path in allow_list
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
@undoc
|
| 237 |
+
@dataclass
|
| 238 |
+
class SelectivePolicy(EvaluationPolicy):
|
| 239 |
+
allowed_getitem: set[InstancesHaveGetItem] = field(default_factory=set)
|
| 240 |
+
allowed_getitem_external: set[tuple[str, ...] | str] = field(default_factory=set)
|
| 241 |
+
|
| 242 |
+
allowed_getattr: set[MayHaveGetattr] = field(default_factory=set)
|
| 243 |
+
allowed_getattr_external: set[tuple[str, ...] | str] = field(default_factory=set)
|
| 244 |
+
|
| 245 |
+
allowed_operations: set = field(default_factory=set)
|
| 246 |
+
allowed_operations_external: set[tuple[str, ...] | str] = field(default_factory=set)
|
| 247 |
+
|
| 248 |
+
allow_getitem_on_types: bool = field(default_factory=bool)
|
| 249 |
+
|
| 250 |
+
_operation_methods_cache: dict[str, set[Callable]] = field(
|
| 251 |
+
default_factory=dict, init=False
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
def can_get_attr(self, value, attr):
|
| 255 |
+
allowed_getattr_external = _coerce_path_to_tuples(self.allowed_getattr_external)
|
| 256 |
+
|
| 257 |
+
has_original_attribute = _has_original_dunder(
|
| 258 |
+
value,
|
| 259 |
+
allowed_types=self.allowed_getattr,
|
| 260 |
+
allowed_methods=self._getattribute_methods,
|
| 261 |
+
allowed_external=allowed_getattr_external,
|
| 262 |
+
method_name="__getattribute__",
|
| 263 |
+
)
|
| 264 |
+
has_original_attr = _has_original_dunder(
|
| 265 |
+
value,
|
| 266 |
+
allowed_types=self.allowed_getattr,
|
| 267 |
+
allowed_methods=self._getattr_methods,
|
| 268 |
+
allowed_external=allowed_getattr_external,
|
| 269 |
+
method_name="__getattr__",
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
accept = False
|
| 273 |
+
|
| 274 |
+
# Many objects do not have `__getattr__`, this is fine.
|
| 275 |
+
if has_original_attr is None and has_original_attribute:
|
| 276 |
+
accept = True
|
| 277 |
+
else:
|
| 278 |
+
# Accept objects without modifications to `__getattr__` and `__getattribute__`
|
| 279 |
+
accept = has_original_attr and has_original_attribute
|
| 280 |
+
|
| 281 |
+
if accept:
|
| 282 |
+
# We still need to check for overridden properties.
|
| 283 |
+
|
| 284 |
+
value_class = type(value)
|
| 285 |
+
if not hasattr(value_class, attr):
|
| 286 |
+
return True
|
| 287 |
+
|
| 288 |
+
class_attr_val = getattr(value_class, attr)
|
| 289 |
+
is_property = isinstance(class_attr_val, property)
|
| 290 |
+
|
| 291 |
+
if not is_property:
|
| 292 |
+
return True
|
| 293 |
+
|
| 294 |
+
# Properties in allowed types are ok (although we do not include any
|
| 295 |
+
# properties in our default allow list currently).
|
| 296 |
+
if type(value) in self.allowed_getattr:
|
| 297 |
+
return True # pragma: no cover
|
| 298 |
+
|
| 299 |
+
# Properties in subclasses of allowed types may be ok if not changed
|
| 300 |
+
for module_name, *access_path in allowed_getattr_external:
|
| 301 |
+
try:
|
| 302 |
+
external_class = _get_external(module_name, access_path)
|
| 303 |
+
external_class_attr_val = getattr(external_class, attr)
|
| 304 |
+
except (KeyError, AttributeError):
|
| 305 |
+
return False # pragma: no cover
|
| 306 |
+
return class_attr_val == external_class_attr_val
|
| 307 |
+
|
| 308 |
+
return False
|
| 309 |
+
|
| 310 |
+
def can_get_item(self, value, item):
|
| 311 |
+
"""Allow accessing `__getiitem__` of allow-listed instances unless it was not modified."""
|
| 312 |
+
allowed_getitem_external = _coerce_path_to_tuples(self.allowed_getitem_external)
|
| 313 |
+
if self.allow_getitem_on_types:
|
| 314 |
+
# e.g. Union[str, int] or Literal[True, 1]
|
| 315 |
+
if isinstance(value, (typing._SpecialForm, typing._BaseGenericAlias)):
|
| 316 |
+
return True
|
| 317 |
+
# PEP 560 e.g. list[str]
|
| 318 |
+
if isinstance(value, type) and hasattr(value, "__class_getitem__"):
|
| 319 |
+
return True
|
| 320 |
+
return _has_original_dunder(
|
| 321 |
+
value,
|
| 322 |
+
allowed_types=self.allowed_getitem,
|
| 323 |
+
allowed_methods=self._getitem_methods,
|
| 324 |
+
allowed_external=allowed_getitem_external,
|
| 325 |
+
method_name="__getitem__",
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
def can_operate(self, dunders: tuple[str, ...], a, b=None):
|
| 329 |
+
allowed_operations_external = _coerce_path_to_tuples(
|
| 330 |
+
self.allowed_operations_external
|
| 331 |
+
)
|
| 332 |
+
objects = [a]
|
| 333 |
+
if b is not None:
|
| 334 |
+
objects.append(b)
|
| 335 |
+
return all(
|
| 336 |
+
[
|
| 337 |
+
_has_original_dunder(
|
| 338 |
+
obj,
|
| 339 |
+
allowed_types=self.allowed_operations,
|
| 340 |
+
allowed_methods=self._operator_dunder_methods(dunder),
|
| 341 |
+
allowed_external=allowed_operations_external,
|
| 342 |
+
method_name=dunder,
|
| 343 |
+
)
|
| 344 |
+
for dunder in dunders
|
| 345 |
+
for obj in objects
|
| 346 |
+
]
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
def _operator_dunder_methods(self, dunder: str) -> set[Callable]:
|
| 350 |
+
if dunder not in self._operation_methods_cache:
|
| 351 |
+
self._operation_methods_cache[dunder] = self._safe_get_methods(
|
| 352 |
+
self.allowed_operations, dunder
|
| 353 |
+
)
|
| 354 |
+
return self._operation_methods_cache[dunder]
|
| 355 |
+
|
| 356 |
+
@cached_property
|
| 357 |
+
def _getitem_methods(self) -> set[Callable]:
|
| 358 |
+
return self._safe_get_methods(self.allowed_getitem, "__getitem__")
|
| 359 |
+
|
| 360 |
+
@cached_property
|
| 361 |
+
def _getattr_methods(self) -> set[Callable]:
|
| 362 |
+
return self._safe_get_methods(self.allowed_getattr, "__getattr__")
|
| 363 |
+
|
| 364 |
+
@cached_property
|
| 365 |
+
def _getattribute_methods(self) -> set[Callable]:
|
| 366 |
+
return self._safe_get_methods(self.allowed_getattr, "__getattribute__")
|
| 367 |
+
|
| 368 |
+
def _safe_get_methods(self, classes, name) -> set[Callable]:
|
| 369 |
+
return {
|
| 370 |
+
method
|
| 371 |
+
for class_ in classes
|
| 372 |
+
for method in [getattr(class_, name, None)]
|
| 373 |
+
if method
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
class _DummyNamedTuple(NamedTuple):
|
| 378 |
+
"""Used internally to retrieve methods of named tuple instance."""
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
EvaluationPolicyName = Literal["forbidden", "minimal", "limited", "unsafe", "dangerous"]
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
@dataclass
|
| 385 |
+
class EvaluationContext:
|
| 386 |
+
#: Local namespace
|
| 387 |
+
locals: dict
|
| 388 |
+
#: Global namespace
|
| 389 |
+
globals: dict
|
| 390 |
+
#: Evaluation policy identifier
|
| 391 |
+
evaluation: EvaluationPolicyName = "forbidden"
|
| 392 |
+
#: Whether the evaluation of code takes place inside of a subscript.
|
| 393 |
+
#: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
|
| 394 |
+
in_subscript: bool = False
|
| 395 |
+
#: Auto import method
|
| 396 |
+
auto_import: Callable[[Sequence[str]], ModuleType] | None = None
|
| 397 |
+
#: Overrides for evaluation policy
|
| 398 |
+
policy_overrides: dict = field(default_factory=dict)
|
| 399 |
+
#: Transient local namespace used to store mocks
|
| 400 |
+
transient_locals: dict = field(default_factory=dict)
|
| 401 |
+
#: Transients of class level
|
| 402 |
+
class_transients: dict | None = None
|
| 403 |
+
#: Instance variable name used in the method definition
|
| 404 |
+
instance_arg_name: str | None = None
|
| 405 |
+
#: Currently associated value
|
| 406 |
+
#: Useful for adding items to _Duck on annotated assignment
|
| 407 |
+
current_value: ast.AST | None = None
|
| 408 |
+
|
| 409 |
+
def replace(self, /, **changes):
|
| 410 |
+
"""Return a new copy of the context, with specified changes"""
|
| 411 |
+
return dataclasses.replace(self, **changes)
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
class _IdentitySubscript:
|
| 415 |
+
"""Returns the key itself when item is requested via subscript."""
|
| 416 |
+
|
| 417 |
+
def __getitem__(self, key):
|
| 418 |
+
return key
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
IDENTITY_SUBSCRIPT = _IdentitySubscript()
|
| 422 |
+
SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__"
|
| 423 |
+
UNKNOWN_SIGNATURE = Signature()
|
| 424 |
+
NOT_EVALUATED = object()
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
class GuardRejection(Exception):
|
| 428 |
+
"""Exception raised when guard rejects evaluation attempt."""
|
| 429 |
+
|
| 430 |
+
pass
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def guarded_eval(code: str, context: EvaluationContext):
|
| 434 |
+
"""Evaluate provided code in the evaluation context.
|
| 435 |
+
|
| 436 |
+
If evaluation policy given by context is set to ``forbidden``
|
| 437 |
+
no evaluation will be performed; if it is set to ``dangerous``
|
| 438 |
+
standard :func:`eval` will be used; finally, for any other,
|
| 439 |
+
policy :func:`eval_node` will be called on parsed AST.
|
| 440 |
+
"""
|
| 441 |
+
locals_ = context.locals
|
| 442 |
+
|
| 443 |
+
if context.evaluation == "forbidden":
|
| 444 |
+
raise GuardRejection("Forbidden mode")
|
| 445 |
+
|
| 446 |
+
# note: not using `ast.literal_eval` as it does not implement
|
| 447 |
+
# getitem at all, for example it fails on simple `[0][1]`
|
| 448 |
+
|
| 449 |
+
if context.in_subscript:
|
| 450 |
+
# syntactic sugar for ellipsis (:) is only available in subscripts
|
| 451 |
+
# so we need to trick the ast parser into thinking that we have
|
| 452 |
+
# a subscript, but we need to be able to later recognise that we did
|
| 453 |
+
# it so we can ignore the actual __getitem__ operation
|
| 454 |
+
if not code:
|
| 455 |
+
return tuple()
|
| 456 |
+
locals_ = locals_.copy()
|
| 457 |
+
locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT
|
| 458 |
+
code = SUBSCRIPT_MARKER + "[" + code + "]"
|
| 459 |
+
context = context.replace(locals=locals_)
|
| 460 |
+
|
| 461 |
+
if context.evaluation == "dangerous":
|
| 462 |
+
return eval(code, context.globals, context.locals)
|
| 463 |
+
|
| 464 |
+
node = ast.parse(code, mode="exec")
|
| 465 |
+
|
| 466 |
+
return eval_node(node, context)
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
BINARY_OP_DUNDERS: dict[type[ast.operator], tuple[str]] = {
|
| 470 |
+
ast.Add: ("__add__",),
|
| 471 |
+
ast.Sub: ("__sub__",),
|
| 472 |
+
ast.Mult: ("__mul__",),
|
| 473 |
+
ast.Div: ("__truediv__",),
|
| 474 |
+
ast.FloorDiv: ("__floordiv__",),
|
| 475 |
+
ast.Mod: ("__mod__",),
|
| 476 |
+
ast.Pow: ("__pow__",),
|
| 477 |
+
ast.LShift: ("__lshift__",),
|
| 478 |
+
ast.RShift: ("__rshift__",),
|
| 479 |
+
ast.BitOr: ("__or__",),
|
| 480 |
+
ast.BitXor: ("__xor__",),
|
| 481 |
+
ast.BitAnd: ("__and__",),
|
| 482 |
+
ast.MatMult: ("__matmul__",),
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
COMP_OP_DUNDERS: dict[type[ast.cmpop], tuple[str, ...]] = {
|
| 486 |
+
ast.Eq: ("__eq__",),
|
| 487 |
+
ast.NotEq: ("__ne__", "__eq__"),
|
| 488 |
+
ast.Lt: ("__lt__", "__gt__"),
|
| 489 |
+
ast.LtE: ("__le__", "__ge__"),
|
| 490 |
+
ast.Gt: ("__gt__", "__lt__"),
|
| 491 |
+
ast.GtE: ("__ge__", "__le__"),
|
| 492 |
+
ast.In: ("__contains__",),
|
| 493 |
+
# Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
UNARY_OP_DUNDERS: dict[type[ast.unaryop], tuple[str, ...]] = {
|
| 497 |
+
ast.USub: ("__neg__",),
|
| 498 |
+
ast.UAdd: ("__pos__",),
|
| 499 |
+
# we have to check both __inv__ and __invert__!
|
| 500 |
+
ast.Invert: ("__invert__", "__inv__"),
|
| 501 |
+
ast.Not: ("__not__",),
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
GENERIC_CONTAINER_TYPES = (dict, list, set, tuple, frozenset)
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
class ImpersonatingDuck:
|
| 508 |
+
"""A dummy class used to create objects of other classes without calling their ``__init__``"""
|
| 509 |
+
|
| 510 |
+
# no-op: override __class__ to impersonate
|
| 511 |
+
|
| 512 |
+
|
| 513 |
+
class _Duck:
|
| 514 |
+
"""A dummy class used to create objects pretending to have given attributes"""
|
| 515 |
+
|
| 516 |
+
def __init__(self, attributes: Optional[dict] = None, items: Optional[dict] = None):
|
| 517 |
+
self.attributes = attributes if attributes is not None else {}
|
| 518 |
+
self.items = items if items is not None else {}
|
| 519 |
+
|
| 520 |
+
def __getattr__(self, attr: str):
|
| 521 |
+
return self.attributes[attr]
|
| 522 |
+
|
| 523 |
+
def __hasattr__(self, attr: str):
|
| 524 |
+
return attr in self.attributes
|
| 525 |
+
|
| 526 |
+
def __dir__(self):
|
| 527 |
+
return [*dir(super), *self.attributes]
|
| 528 |
+
|
| 529 |
+
def __getitem__(self, key: str):
|
| 530 |
+
return self.items[key]
|
| 531 |
+
|
| 532 |
+
def __hasitem__(self, key: str):
|
| 533 |
+
return self.items[key]
|
| 534 |
+
|
| 535 |
+
def _ipython_key_completions_(self):
|
| 536 |
+
return self.items.keys()
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def _find_dunder(node_op, dunders) -> Union[tuple[str, ...], None]:
|
| 540 |
+
dunder = None
|
| 541 |
+
for op, candidate_dunder in dunders.items():
|
| 542 |
+
if isinstance(node_op, op):
|
| 543 |
+
dunder = candidate_dunder
|
| 544 |
+
return dunder
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def get_policy(context: EvaluationContext) -> EvaluationPolicy:
|
| 548 |
+
policy = copy(EVALUATION_POLICIES[context.evaluation])
|
| 549 |
+
|
| 550 |
+
for key, value in context.policy_overrides.items():
|
| 551 |
+
if hasattr(policy, key):
|
| 552 |
+
setattr(policy, key, value)
|
| 553 |
+
return policy
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def _validate_policy_overrides(
|
| 557 |
+
policy_name: EvaluationPolicyName, policy_overrides: dict
|
| 558 |
+
) -> bool:
|
| 559 |
+
policy = EVALUATION_POLICIES[policy_name]
|
| 560 |
+
|
| 561 |
+
all_good = True
|
| 562 |
+
for key, value in policy_overrides.items():
|
| 563 |
+
if not hasattr(policy, key):
|
| 564 |
+
warnings.warn(
|
| 565 |
+
f"Override {key!r} is not valid with {policy_name!r} evaluation policy"
|
| 566 |
+
)
|
| 567 |
+
all_good = False
|
| 568 |
+
return all_good
|
| 569 |
+
|
| 570 |
+
|
| 571 |
+
def _is_type_annotation(obj) -> bool:
|
| 572 |
+
"""
|
| 573 |
+
Returns True if obj is a type annotation, False otherwise.
|
| 574 |
+
"""
|
| 575 |
+
if isinstance(obj, type):
|
| 576 |
+
return True
|
| 577 |
+
if isinstance(obj, types.GenericAlias):
|
| 578 |
+
return True
|
| 579 |
+
if hasattr(types, "UnionType") and isinstance(obj, types.UnionType):
|
| 580 |
+
return True
|
| 581 |
+
if isinstance(obj, (typing._SpecialForm, typing._BaseGenericAlias)):
|
| 582 |
+
return True
|
| 583 |
+
if isinstance(obj, typing.TypeVar):
|
| 584 |
+
return True
|
| 585 |
+
# Types that support __class_getitem__
|
| 586 |
+
if isinstance(obj, type) and hasattr(obj, "__class_getitem__"):
|
| 587 |
+
return True
|
| 588 |
+
# Fallback: check if get_origin returns something
|
| 589 |
+
if hasattr(typing, "get_origin") and get_origin(obj) is not None:
|
| 590 |
+
return True
|
| 591 |
+
|
| 592 |
+
return False
|
| 593 |
+
|
| 594 |
+
|
| 595 |
+
def _handle_assign(node: ast.Assign, context: EvaluationContext):
|
| 596 |
+
value = eval_node(node.value, context)
|
| 597 |
+
transient_locals = context.transient_locals
|
| 598 |
+
policy = get_policy(context)
|
| 599 |
+
class_transients = context.class_transients
|
| 600 |
+
for target in node.targets:
|
| 601 |
+
if isinstance(target, (ast.Tuple, ast.List)):
|
| 602 |
+
# Handle unpacking assignment
|
| 603 |
+
values = list(value)
|
| 604 |
+
targets = target.elts
|
| 605 |
+
starred = [i for i, t in enumerate(targets) if isinstance(t, ast.Starred)]
|
| 606 |
+
|
| 607 |
+
# Unified handling: treat no starred as starred at end
|
| 608 |
+
star_or_last_idx = starred[0] if starred else len(targets)
|
| 609 |
+
|
| 610 |
+
# Before starred
|
| 611 |
+
for i in range(star_or_last_idx):
|
| 612 |
+
# Check for self.x assignment
|
| 613 |
+
if _is_instance_attribute_assignment(targets[i], context):
|
| 614 |
+
class_transients[targets[i].attr] = values[i]
|
| 615 |
+
else:
|
| 616 |
+
transient_locals[targets[i].id] = values[i]
|
| 617 |
+
|
| 618 |
+
# Starred if exists
|
| 619 |
+
if starred:
|
| 620 |
+
end = len(values) - (len(targets) - star_or_last_idx - 1)
|
| 621 |
+
if _is_instance_attribute_assignment(
|
| 622 |
+
targets[star_or_last_idx], context
|
| 623 |
+
):
|
| 624 |
+
class_transients[targets[star_or_last_idx].attr] = values[
|
| 625 |
+
star_or_last_idx:end
|
| 626 |
+
]
|
| 627 |
+
else:
|
| 628 |
+
transient_locals[targets[star_or_last_idx].value.id] = values[
|
| 629 |
+
star_or_last_idx:end
|
| 630 |
+
]
|
| 631 |
+
|
| 632 |
+
# After starred
|
| 633 |
+
for i in range(star_or_last_idx + 1, len(targets)):
|
| 634 |
+
if _is_instance_attribute_assignment(targets[i], context):
|
| 635 |
+
class_transients[targets[i].attr] = values[
|
| 636 |
+
len(values) - (len(targets) - i)
|
| 637 |
+
]
|
| 638 |
+
else:
|
| 639 |
+
transient_locals[targets[i].id] = values[
|
| 640 |
+
len(values) - (len(targets) - i)
|
| 641 |
+
]
|
| 642 |
+
elif isinstance(target, ast.Subscript):
|
| 643 |
+
if isinstance(target.value, ast.Name):
|
| 644 |
+
name = target.value.id
|
| 645 |
+
container = transient_locals.get(name)
|
| 646 |
+
if container is None:
|
| 647 |
+
container = context.locals.get(name)
|
| 648 |
+
if container is None:
|
| 649 |
+
container = context.globals.get(name)
|
| 650 |
+
if container is None:
|
| 651 |
+
raise NameError(
|
| 652 |
+
f"{name} not found in locals, globals, nor builtins"
|
| 653 |
+
)
|
| 654 |
+
storage_dict = transient_locals
|
| 655 |
+
storage_key = name
|
| 656 |
+
elif isinstance(
|
| 657 |
+
target.value, ast.Attribute
|
| 658 |
+
) and _is_instance_attribute_assignment(target.value, context):
|
| 659 |
+
attr = target.value.attr
|
| 660 |
+
container = class_transients.get(attr, None)
|
| 661 |
+
if container is None:
|
| 662 |
+
raise NameError(f"{attr} not found in class transients")
|
| 663 |
+
storage_dict = class_transients
|
| 664 |
+
storage_key = attr
|
| 665 |
+
else:
|
| 666 |
+
return
|
| 667 |
+
|
| 668 |
+
key = eval_node(target.slice, context)
|
| 669 |
+
attributes = (
|
| 670 |
+
dict.fromkeys(dir(container))
|
| 671 |
+
if policy.can_call(container.__dir__)
|
| 672 |
+
else {}
|
| 673 |
+
)
|
| 674 |
+
items = {}
|
| 675 |
+
|
| 676 |
+
if policy.can_get_item(container, None):
|
| 677 |
+
try:
|
| 678 |
+
items = dict(container.items())
|
| 679 |
+
except Exception:
|
| 680 |
+
pass
|
| 681 |
+
|
| 682 |
+
items[key] = value
|
| 683 |
+
duck_container = _Duck(attributes=attributes, items=items)
|
| 684 |
+
storage_dict[storage_key] = duck_container
|
| 685 |
+
elif _is_instance_attribute_assignment(target, context):
|
| 686 |
+
class_transients[target.attr] = value
|
| 687 |
+
else:
|
| 688 |
+
transient_locals[target.id] = value
|
| 689 |
+
return None
|
| 690 |
+
|
| 691 |
+
|
| 692 |
+
def _handle_annassign(node, context):
|
| 693 |
+
context_with_value = context.replace(current_value=getattr(node, "value", None))
|
| 694 |
+
annotation_result = eval_node(node.annotation, context_with_value)
|
| 695 |
+
if _is_type_annotation(annotation_result):
|
| 696 |
+
annotation_value = _resolve_annotation(annotation_result, context)
|
| 697 |
+
# Use Value for generic types
|
| 698 |
+
use_value = (
|
| 699 |
+
isinstance(annotation_value, GENERIC_CONTAINER_TYPES) and node.value is not None
|
| 700 |
+
)
|
| 701 |
+
else:
|
| 702 |
+
annotation_value = annotation_result
|
| 703 |
+
use_value = False
|
| 704 |
+
|
| 705 |
+
# LOCAL VARIABLE
|
| 706 |
+
if getattr(node, "simple", False) and isinstance(node.target, ast.Name):
|
| 707 |
+
name = node.target.id
|
| 708 |
+
if use_value:
|
| 709 |
+
return _handle_assign(
|
| 710 |
+
ast.Assign(targets=[node.target], value=node.value), context
|
| 711 |
+
)
|
| 712 |
+
context.transient_locals[name] = annotation_value
|
| 713 |
+
return None
|
| 714 |
+
|
| 715 |
+
# INSTANCE ATTRIBUTE
|
| 716 |
+
if _is_instance_attribute_assignment(node.target, context):
|
| 717 |
+
attr = node.target.attr
|
| 718 |
+
if use_value:
|
| 719 |
+
return _handle_assign(
|
| 720 |
+
ast.Assign(targets=[node.target], value=node.value), context
|
| 721 |
+
)
|
| 722 |
+
context.class_transients[attr] = annotation_value
|
| 723 |
+
return None
|
| 724 |
+
|
| 725 |
+
return None
|
| 726 |
+
|
| 727 |
+
def _extract_args_and_kwargs(node: ast.Call, context: EvaluationContext):
|
| 728 |
+
args = [eval_node(arg, context) for arg in node.args]
|
| 729 |
+
kwargs = {
|
| 730 |
+
k: v
|
| 731 |
+
for kw in node.keywords
|
| 732 |
+
for k, v in (
|
| 733 |
+
{kw.arg: eval_node(kw.value, context)}
|
| 734 |
+
if kw.arg
|
| 735 |
+
else eval_node(kw.value, context)
|
| 736 |
+
).items()
|
| 737 |
+
}
|
| 738 |
+
return args, kwargs
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
def _is_instance_attribute_assignment(
|
| 742 |
+
target: ast.AST, context: EvaluationContext
|
| 743 |
+
) -> bool:
|
| 744 |
+
"""Return True if target is an attribute access on the instance argument."""
|
| 745 |
+
return (
|
| 746 |
+
context.class_transients is not None
|
| 747 |
+
and context.instance_arg_name is not None
|
| 748 |
+
and isinstance(target, ast.Attribute)
|
| 749 |
+
and isinstance(getattr(target, "value", None), ast.Name)
|
| 750 |
+
and getattr(target.value, "id", None) == context.instance_arg_name
|
| 751 |
+
)
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
def _get_coroutine_attributes() -> dict[str, Optional[object]]:
|
| 755 |
+
async def _dummy():
|
| 756 |
+
return None
|
| 757 |
+
|
| 758 |
+
coro = _dummy()
|
| 759 |
+
try:
|
| 760 |
+
return {attr: getattr(coro, attr, None) for attr in dir(coro)}
|
| 761 |
+
finally:
|
| 762 |
+
coro.close()
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
|
| 766 |
+
"""Evaluate AST node in provided context.
|
| 767 |
+
|
| 768 |
+
Applies evaluation restrictions defined in the context. Currently does not support evaluation of functions with keyword arguments.
|
| 769 |
+
|
| 770 |
+
Does not evaluate actions that always have side effects:
|
| 771 |
+
|
| 772 |
+
- class definitions (``class sth: ...``)
|
| 773 |
+
- function definitions (``def sth: ...``)
|
| 774 |
+
- variable assignments (``x = 1``)
|
| 775 |
+
- augmented assignments (``x += 1``)
|
| 776 |
+
- deletions (``del x``)
|
| 777 |
+
|
| 778 |
+
Does not evaluate operations which do not return values:
|
| 779 |
+
|
| 780 |
+
- assertions (``assert x``)
|
| 781 |
+
- pass (``pass``)
|
| 782 |
+
- imports (``import x``)
|
| 783 |
+
- control flow:
|
| 784 |
+
|
| 785 |
+
- conditionals (``if x:``) except for ternary IfExp (``a if x else b``)
|
| 786 |
+
- loops (``for`` and ``while``)
|
| 787 |
+
- exception handling
|
| 788 |
+
|
| 789 |
+
The purpose of this function is to guard against unwanted side-effects;
|
| 790 |
+
it does not give guarantees on protection from malicious code execution.
|
| 791 |
+
"""
|
| 792 |
+
policy = get_policy(context)
|
| 793 |
+
|
| 794 |
+
if node is None:
|
| 795 |
+
return None
|
| 796 |
+
if isinstance(node, (ast.Interactive, ast.Module)):
|
| 797 |
+
result = None
|
| 798 |
+
for child_node in node.body:
|
| 799 |
+
result = eval_node(child_node, context)
|
| 800 |
+
return result
|
| 801 |
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 802 |
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
| 803 |
+
func_locals = context.transient_locals.copy()
|
| 804 |
+
func_context = context.replace(transient_locals=func_locals)
|
| 805 |
+
is_property = False
|
| 806 |
+
is_static = False
|
| 807 |
+
is_classmethod = False
|
| 808 |
+
for decorator_node in node.decorator_list:
|
| 809 |
+
try:
|
| 810 |
+
decorator = eval_node(decorator_node, context)
|
| 811 |
+
except NameError:
|
| 812 |
+
# if the decorator is not yet defined this is fine
|
| 813 |
+
# especialy because we don't handle imports yet
|
| 814 |
+
continue
|
| 815 |
+
if decorator is property:
|
| 816 |
+
is_property = True
|
| 817 |
+
elif decorator is staticmethod:
|
| 818 |
+
is_static = True
|
| 819 |
+
elif decorator is classmethod:
|
| 820 |
+
is_classmethod = True
|
| 821 |
+
|
| 822 |
+
if func_context.class_transients is not None:
|
| 823 |
+
if not is_static and not is_classmethod:
|
| 824 |
+
func_context.instance_arg_name = (
|
| 825 |
+
node.args.args[0].arg if node.args.args else None
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
return_type = eval_node(node.returns, context=context)
|
| 829 |
+
|
| 830 |
+
for child_node in node.body:
|
| 831 |
+
eval_node(child_node, func_context)
|
| 832 |
+
|
| 833 |
+
if is_property:
|
| 834 |
+
if return_type is not None:
|
| 835 |
+
if _is_type_annotation(return_type):
|
| 836 |
+
context.transient_locals[node.name] = _resolve_annotation(
|
| 837 |
+
return_type, context
|
| 838 |
+
)
|
| 839 |
+
else:
|
| 840 |
+
context.transient_locals[node.name] = return_type
|
| 841 |
+
else:
|
| 842 |
+
return_value = _infer_return_value(node, func_context)
|
| 843 |
+
context.transient_locals[node.name] = return_value
|
| 844 |
+
|
| 845 |
+
return None
|
| 846 |
+
|
| 847 |
+
def dummy_function(*args, **kwargs):
|
| 848 |
+
pass
|
| 849 |
+
|
| 850 |
+
if return_type is not None:
|
| 851 |
+
if _is_type_annotation(return_type):
|
| 852 |
+
dummy_function.__annotations__["return"] = return_type
|
| 853 |
+
else:
|
| 854 |
+
dummy_function.__inferred_return__ = return_type
|
| 855 |
+
else:
|
| 856 |
+
inferred_return = _infer_return_value(node, func_context)
|
| 857 |
+
if inferred_return is not None:
|
| 858 |
+
dummy_function.__inferred_return__ = inferred_return
|
| 859 |
+
|
| 860 |
+
dummy_function.__name__ = node.name
|
| 861 |
+
dummy_function.__node__ = node
|
| 862 |
+
dummy_function.__is_async__ = is_async
|
| 863 |
+
context.transient_locals[node.name] = dummy_function
|
| 864 |
+
return None
|
| 865 |
+
if isinstance(node, ast.Lambda):
|
| 866 |
+
|
| 867 |
+
def dummy_function(*args, **kwargs):
|
| 868 |
+
pass
|
| 869 |
+
|
| 870 |
+
dummy_function.__inferred_return__ = eval_node(node.body, context)
|
| 871 |
+
return dummy_function
|
| 872 |
+
if isinstance(node, ast.ClassDef):
|
| 873 |
+
# TODO support class decorators?
|
| 874 |
+
class_locals = {}
|
| 875 |
+
outer_locals = context.locals.copy()
|
| 876 |
+
outer_locals.update(context.transient_locals)
|
| 877 |
+
class_context = context.replace(
|
| 878 |
+
transient_locals=class_locals, locals=outer_locals
|
| 879 |
+
)
|
| 880 |
+
class_context.class_transients = class_locals
|
| 881 |
+
for child_node in node.body:
|
| 882 |
+
eval_node(child_node, class_context)
|
| 883 |
+
bases = tuple([eval_node(base, context) for base in node.bases])
|
| 884 |
+
dummy_class = type(node.name, bases, class_locals)
|
| 885 |
+
context.transient_locals[node.name] = dummy_class
|
| 886 |
+
return None
|
| 887 |
+
if isinstance(node, ast.Await):
|
| 888 |
+
value = eval_node(node.value, context)
|
| 889 |
+
if hasattr(value, "__awaited_type__"):
|
| 890 |
+
return value.__awaited_type__
|
| 891 |
+
return value
|
| 892 |
+
if isinstance(node, ast.While):
|
| 893 |
+
loop_locals = context.transient_locals.copy()
|
| 894 |
+
loop_context = context.replace(transient_locals=loop_locals)
|
| 895 |
+
|
| 896 |
+
result = None
|
| 897 |
+
for stmt in node.body:
|
| 898 |
+
result = eval_node(stmt, loop_context)
|
| 899 |
+
|
| 900 |
+
policy = get_policy(context)
|
| 901 |
+
merged_locals = _merge_dicts_by_key(
|
| 902 |
+
[loop_locals, context.transient_locals.copy()], policy
|
| 903 |
+
)
|
| 904 |
+
context.transient_locals.update(merged_locals)
|
| 905 |
+
|
| 906 |
+
return result
|
| 907 |
+
if isinstance(node, ast.For):
|
| 908 |
+
try:
|
| 909 |
+
iterable = eval_node(node.iter, context)
|
| 910 |
+
except Exception:
|
| 911 |
+
iterable = None
|
| 912 |
+
|
| 913 |
+
sample = None
|
| 914 |
+
if iterable is not None:
|
| 915 |
+
try:
|
| 916 |
+
if policy.can_call(getattr(iterable, "__iter__", None)):
|
| 917 |
+
sample = next(iter(iterable))
|
| 918 |
+
except Exception:
|
| 919 |
+
sample = None
|
| 920 |
+
|
| 921 |
+
loop_locals = context.transient_locals.copy()
|
| 922 |
+
loop_context = context.replace(transient_locals=loop_locals)
|
| 923 |
+
|
| 924 |
+
if sample is not None:
|
| 925 |
+
try:
|
| 926 |
+
fake_assign = ast.Assign(
|
| 927 |
+
targets=[node.target], value=ast.Constant(value=sample)
|
| 928 |
+
)
|
| 929 |
+
_handle_assign(fake_assign, loop_context)
|
| 930 |
+
except Exception:
|
| 931 |
+
pass
|
| 932 |
+
|
| 933 |
+
result = None
|
| 934 |
+
for stmt in node.body:
|
| 935 |
+
result = eval_node(stmt, loop_context)
|
| 936 |
+
|
| 937 |
+
policy = get_policy(context)
|
| 938 |
+
merged_locals = _merge_dicts_by_key(
|
| 939 |
+
[loop_locals, context.transient_locals.copy()], policy
|
| 940 |
+
)
|
| 941 |
+
context.transient_locals.update(merged_locals)
|
| 942 |
+
|
| 943 |
+
return result
|
| 944 |
+
if isinstance(node, ast.If):
|
| 945 |
+
branches = []
|
| 946 |
+
current = node
|
| 947 |
+
result = None
|
| 948 |
+
while True:
|
| 949 |
+
branch_locals = context.transient_locals.copy()
|
| 950 |
+
branch_context = context.replace(transient_locals=branch_locals)
|
| 951 |
+
for stmt in current.body:
|
| 952 |
+
result = eval_node(stmt, branch_context)
|
| 953 |
+
branches.append(branch_locals)
|
| 954 |
+
if not current.orelse:
|
| 955 |
+
break
|
| 956 |
+
elif len(current.orelse) == 1 and isinstance(current.orelse[0], ast.If):
|
| 957 |
+
# It's an elif - continue loop
|
| 958 |
+
current = current.orelse[0]
|
| 959 |
+
else:
|
| 960 |
+
# It's an else block - process and break
|
| 961 |
+
else_locals = context.transient_locals.copy()
|
| 962 |
+
else_context = context.replace(transient_locals=else_locals)
|
| 963 |
+
for stmt in current.orelse:
|
| 964 |
+
result = eval_node(stmt, else_context)
|
| 965 |
+
branches.append(else_locals)
|
| 966 |
+
break
|
| 967 |
+
branches.append(context.transient_locals.copy())
|
| 968 |
+
policy = get_policy(context)
|
| 969 |
+
merged_locals = _merge_dicts_by_key(branches, policy)
|
| 970 |
+
context.transient_locals.update(merged_locals)
|
| 971 |
+
return result
|
| 972 |
+
if isinstance(node, ast.Assign):
|
| 973 |
+
return _handle_assign(node, context)
|
| 974 |
+
if isinstance(node, ast.AnnAssign):
|
| 975 |
+
return _handle_annassign(node, context)
|
| 976 |
+
if isinstance(node, ast.Expression):
|
| 977 |
+
return eval_node(node.body, context)
|
| 978 |
+
if isinstance(node, ast.Expr):
|
| 979 |
+
return eval_node(node.value, context)
|
| 980 |
+
if isinstance(node, ast.Pass):
|
| 981 |
+
return None
|
| 982 |
+
if isinstance(node, ast.Import):
|
| 983 |
+
# TODO: populate transient_locals
|
| 984 |
+
return None
|
| 985 |
+
if isinstance(node, (ast.AugAssign, ast.Delete)):
|
| 986 |
+
return None
|
| 987 |
+
if isinstance(node, (ast.Global, ast.Nonlocal)):
|
| 988 |
+
return None
|
| 989 |
+
if isinstance(node, ast.BinOp):
|
| 990 |
+
left = eval_node(node.left, context)
|
| 991 |
+
right = eval_node(node.right, context)
|
| 992 |
+
if (
|
| 993 |
+
isinstance(node.op, ast.BitOr)
|
| 994 |
+
and _is_type_annotation(left)
|
| 995 |
+
and _is_type_annotation(right)
|
| 996 |
+
):
|
| 997 |
+
left_duck = (
|
| 998 |
+
_Duck(dict.fromkeys(dir(left)))
|
| 999 |
+
if policy.can_call(left.__dir__)
|
| 1000 |
+
else _Duck()
|
| 1001 |
+
)
|
| 1002 |
+
right_duck = (
|
| 1003 |
+
_Duck(dict.fromkeys(dir(right)))
|
| 1004 |
+
if policy.can_call(right.__dir__)
|
| 1005 |
+
else _Duck()
|
| 1006 |
+
)
|
| 1007 |
+
value_node = context.current_value
|
| 1008 |
+
if value_node is not None and isinstance(value_node, ast.Dict):
|
| 1009 |
+
if dict in [left, right]:
|
| 1010 |
+
return _merge_values(
|
| 1011 |
+
[left_duck, right_duck, ast.literal_eval(value_node)],
|
| 1012 |
+
policy=get_policy(context),
|
| 1013 |
+
)
|
| 1014 |
+
return _merge_values([left_duck, right_duck], policy=get_policy(context))
|
| 1015 |
+
dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
|
| 1016 |
+
if dunders:
|
| 1017 |
+
if policy.can_operate(dunders, left, right):
|
| 1018 |
+
return getattr(left, dunders[0])(right)
|
| 1019 |
+
else:
|
| 1020 |
+
raise GuardRejection(
|
| 1021 |
+
f"Operation (`{dunders}`) for",
|
| 1022 |
+
type(left),
|
| 1023 |
+
f"not allowed in {context.evaluation} mode",
|
| 1024 |
+
)
|
| 1025 |
+
if isinstance(node, ast.Compare):
|
| 1026 |
+
left = eval_node(node.left, context)
|
| 1027 |
+
all_true = True
|
| 1028 |
+
negate = False
|
| 1029 |
+
for op, right in zip(node.ops, node.comparators):
|
| 1030 |
+
right = eval_node(right, context)
|
| 1031 |
+
dunder = None
|
| 1032 |
+
dunders = _find_dunder(op, COMP_OP_DUNDERS)
|
| 1033 |
+
if not dunders:
|
| 1034 |
+
if isinstance(op, ast.NotIn):
|
| 1035 |
+
dunders = COMP_OP_DUNDERS[ast.In]
|
| 1036 |
+
negate = True
|
| 1037 |
+
if isinstance(op, ast.Is):
|
| 1038 |
+
dunder = "is_"
|
| 1039 |
+
if isinstance(op, ast.IsNot):
|
| 1040 |
+
dunder = "is_"
|
| 1041 |
+
negate = True
|
| 1042 |
+
if not dunder and dunders:
|
| 1043 |
+
dunder = dunders[0]
|
| 1044 |
+
if dunder:
|
| 1045 |
+
a, b = (right, left) if dunder == "__contains__" else (left, right)
|
| 1046 |
+
if dunder == "is_" or dunders and policy.can_operate(dunders, a, b):
|
| 1047 |
+
result = getattr(operator, dunder)(a, b)
|
| 1048 |
+
if negate:
|
| 1049 |
+
result = not result
|
| 1050 |
+
if not result:
|
| 1051 |
+
all_true = False
|
| 1052 |
+
left = right
|
| 1053 |
+
else:
|
| 1054 |
+
raise GuardRejection(
|
| 1055 |
+
f"Comparison (`{dunder}`) for",
|
| 1056 |
+
type(left),
|
| 1057 |
+
f"not allowed in {context.evaluation} mode",
|
| 1058 |
+
)
|
| 1059 |
+
else:
|
| 1060 |
+
raise ValueError(
|
| 1061 |
+
f"Comparison `{dunder}` not supported"
|
| 1062 |
+
) # pragma: no cover
|
| 1063 |
+
return all_true
|
| 1064 |
+
if isinstance(node, ast.Constant):
|
| 1065 |
+
return node.value
|
| 1066 |
+
if isinstance(node, ast.Tuple):
|
| 1067 |
+
return tuple(eval_node(e, context) for e in node.elts)
|
| 1068 |
+
if isinstance(node, ast.List):
|
| 1069 |
+
return [eval_node(e, context) for e in node.elts]
|
| 1070 |
+
if isinstance(node, ast.Set):
|
| 1071 |
+
return {eval_node(e, context) for e in node.elts}
|
| 1072 |
+
if isinstance(node, ast.Dict):
|
| 1073 |
+
return dict(
|
| 1074 |
+
zip(
|
| 1075 |
+
[eval_node(k, context) for k in node.keys],
|
| 1076 |
+
[eval_node(v, context) for v in node.values],
|
| 1077 |
+
)
|
| 1078 |
+
)
|
| 1079 |
+
if isinstance(node, ast.Slice):
|
| 1080 |
+
return slice(
|
| 1081 |
+
eval_node(node.lower, context),
|
| 1082 |
+
eval_node(node.upper, context),
|
| 1083 |
+
eval_node(node.step, context),
|
| 1084 |
+
)
|
| 1085 |
+
if isinstance(node, ast.UnaryOp):
|
| 1086 |
+
value = eval_node(node.operand, context)
|
| 1087 |
+
dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
|
| 1088 |
+
if dunders:
|
| 1089 |
+
if policy.can_operate(dunders, value):
|
| 1090 |
+
try:
|
| 1091 |
+
return getattr(value, dunders[0])()
|
| 1092 |
+
except AttributeError:
|
| 1093 |
+
raise TypeError(
|
| 1094 |
+
f"bad operand type for unary {node.op}: {type(value)}"
|
| 1095 |
+
)
|
| 1096 |
+
else:
|
| 1097 |
+
raise GuardRejection(
|
| 1098 |
+
f"Operation (`{dunders}`) for",
|
| 1099 |
+
type(value),
|
| 1100 |
+
f"not allowed in {context.evaluation} mode",
|
| 1101 |
+
)
|
| 1102 |
+
if isinstance(node, ast.Subscript):
|
| 1103 |
+
value = eval_node(node.value, context)
|
| 1104 |
+
slice_ = eval_node(node.slice, context)
|
| 1105 |
+
if policy.can_get_item(value, slice_):
|
| 1106 |
+
return value[slice_]
|
| 1107 |
+
raise GuardRejection(
|
| 1108 |
+
"Subscript access (`__getitem__`) for",
|
| 1109 |
+
type(value), # not joined to avoid calling `repr`
|
| 1110 |
+
f" not allowed in {context.evaluation} mode",
|
| 1111 |
+
)
|
| 1112 |
+
if isinstance(node, ast.Name):
|
| 1113 |
+
return _eval_node_name(node.id, context)
|
| 1114 |
+
if isinstance(node, ast.Attribute):
|
| 1115 |
+
if (
|
| 1116 |
+
context.class_transients is not None
|
| 1117 |
+
and isinstance(node.value, ast.Name)
|
| 1118 |
+
and node.value.id == context.instance_arg_name
|
| 1119 |
+
):
|
| 1120 |
+
return context.class_transients.get(node.attr)
|
| 1121 |
+
value = eval_node(node.value, context)
|
| 1122 |
+
if policy.can_get_attr(value, node.attr):
|
| 1123 |
+
return getattr(value, node.attr)
|
| 1124 |
+
try:
|
| 1125 |
+
cls = (
|
| 1126 |
+
value if isinstance(value, type) else getattr(value, "__class__", None)
|
| 1127 |
+
)
|
| 1128 |
+
if cls is not None:
|
| 1129 |
+
resolved_hints = get_type_hints(
|
| 1130 |
+
cls,
|
| 1131 |
+
globalns=(context.globals or {}),
|
| 1132 |
+
localns=(context.locals or {}),
|
| 1133 |
+
)
|
| 1134 |
+
if node.attr in resolved_hints:
|
| 1135 |
+
annotated = resolved_hints[node.attr]
|
| 1136 |
+
return _resolve_annotation(annotated, context)
|
| 1137 |
+
except Exception:
|
| 1138 |
+
# Fall through to the guard rejection
|
| 1139 |
+
pass
|
| 1140 |
+
raise GuardRejection(
|
| 1141 |
+
"Attribute access (`__getattr__`) for",
|
| 1142 |
+
type(value), # not joined to avoid calling `repr`
|
| 1143 |
+
f"not allowed in {context.evaluation} mode",
|
| 1144 |
+
)
|
| 1145 |
+
if isinstance(node, ast.IfExp):
|
| 1146 |
+
test = eval_node(node.test, context)
|
| 1147 |
+
if test:
|
| 1148 |
+
return eval_node(node.body, context)
|
| 1149 |
+
else:
|
| 1150 |
+
return eval_node(node.orelse, context)
|
| 1151 |
+
if isinstance(node, ast.Call):
|
| 1152 |
+
func = eval_node(node.func, context)
|
| 1153 |
+
if policy.can_call(func):
|
| 1154 |
+
args, kwargs = _extract_args_and_kwargs(node, context)
|
| 1155 |
+
return func(*args, **kwargs)
|
| 1156 |
+
if isclass(func):
|
| 1157 |
+
# this code path gets entered when calling class e.g. `MyClass()`
|
| 1158 |
+
# or `my_instance.__class__()` - in both cases `func` is `MyClass`.
|
| 1159 |
+
# Should return `MyClass` if `__new__` is not overridden,
|
| 1160 |
+
# otherwise whatever `__new__` return type is.
|
| 1161 |
+
overridden_return_type = _eval_return_type(func.__new__, node, context)
|
| 1162 |
+
if overridden_return_type is not NOT_EVALUATED:
|
| 1163 |
+
return overridden_return_type
|
| 1164 |
+
return _create_duck_for_heap_type(func)
|
| 1165 |
+
else:
|
| 1166 |
+
inferred_return = getattr(func, "__inferred_return__", NOT_EVALUATED)
|
| 1167 |
+
return_type = _eval_return_type(func, node, context)
|
| 1168 |
+
if getattr(func, "__is_async__", False):
|
| 1169 |
+
awaited_type = (
|
| 1170 |
+
inferred_return if inferred_return is not None else return_type
|
| 1171 |
+
)
|
| 1172 |
+
coroutine_duck = _Duck(attributes=_get_coroutine_attributes())
|
| 1173 |
+
coroutine_duck.__awaited_type__ = awaited_type
|
| 1174 |
+
return coroutine_duck
|
| 1175 |
+
if inferred_return is not NOT_EVALUATED:
|
| 1176 |
+
return inferred_return
|
| 1177 |
+
if return_type is not NOT_EVALUATED:
|
| 1178 |
+
return return_type
|
| 1179 |
+
raise GuardRejection(
|
| 1180 |
+
"Call for",
|
| 1181 |
+
func, # not joined to avoid calling `repr`
|
| 1182 |
+
f"not allowed in {context.evaluation} mode",
|
| 1183 |
+
)
|
| 1184 |
+
if isinstance(node, ast.Assert):
|
| 1185 |
+
# message is always the second item, so if it is defined user would be completing
|
| 1186 |
+
# on the message, not on the assertion test
|
| 1187 |
+
if node.msg:
|
| 1188 |
+
return eval_node(node.msg, context)
|
| 1189 |
+
return eval_node(node.test, context)
|
| 1190 |
+
return None
|
| 1191 |
+
|
| 1192 |
+
|
| 1193 |
+
def _merge_dicts_by_key(dicts: list, policy: EvaluationPolicy):
|
| 1194 |
+
"""Merge multiple dictionaries, combining values for each key."""
|
| 1195 |
+
if len(dicts) == 1:
|
| 1196 |
+
return dicts[0]
|
| 1197 |
+
|
| 1198 |
+
all_keys = set()
|
| 1199 |
+
for d in dicts:
|
| 1200 |
+
all_keys.update(d.keys())
|
| 1201 |
+
|
| 1202 |
+
merged = {}
|
| 1203 |
+
for key in all_keys:
|
| 1204 |
+
values = [d[key] for d in dicts if key in d]
|
| 1205 |
+
if values:
|
| 1206 |
+
merged[key] = _merge_values(values, policy)
|
| 1207 |
+
|
| 1208 |
+
return merged
|
| 1209 |
+
|
| 1210 |
+
|
| 1211 |
+
def _merge_values(values, policy: EvaluationPolicy):
|
| 1212 |
+
"""Recursively merge multiple values, combining attributes and dict items."""
|
| 1213 |
+
if len(values) == 1:
|
| 1214 |
+
return values[0]
|
| 1215 |
+
|
| 1216 |
+
types = {type(v) for v in values}
|
| 1217 |
+
merged_items = None
|
| 1218 |
+
key_values = {}
|
| 1219 |
+
attributes = set()
|
| 1220 |
+
for v in values:
|
| 1221 |
+
if policy.can_call(v.__dir__):
|
| 1222 |
+
attributes.update(dir(v))
|
| 1223 |
+
try:
|
| 1224 |
+
if policy.can_call(v.items):
|
| 1225 |
+
try:
|
| 1226 |
+
for k, val in v.items():
|
| 1227 |
+
key_values.setdefault(k, []).append(val)
|
| 1228 |
+
except Exception as e:
|
| 1229 |
+
pass
|
| 1230 |
+
elif policy.can_call(v.keys):
|
| 1231 |
+
try:
|
| 1232 |
+
for k in v.keys():
|
| 1233 |
+
key_values.setdefault(k, []).append(None)
|
| 1234 |
+
except Exception as e:
|
| 1235 |
+
pass
|
| 1236 |
+
except Exception as e:
|
| 1237 |
+
pass
|
| 1238 |
+
|
| 1239 |
+
if key_values:
|
| 1240 |
+
merged_items = {
|
| 1241 |
+
k: _merge_values(vals, policy) if vals[0] is not None else None
|
| 1242 |
+
for k, vals in key_values.items()
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
if len(types) == 1:
|
| 1246 |
+
t = next(iter(types))
|
| 1247 |
+
if t not in (dict,) and not (
|
| 1248 |
+
hasattr(next(iter(values)), "__getitem__")
|
| 1249 |
+
and (
|
| 1250 |
+
hasattr(next(iter(values)), "items")
|
| 1251 |
+
or hasattr(next(iter(values)), "keys")
|
| 1252 |
+
)
|
| 1253 |
+
):
|
| 1254 |
+
if t in (list, set, tuple):
|
| 1255 |
+
return t
|
| 1256 |
+
return values[0]
|
| 1257 |
+
|
| 1258 |
+
return _Duck(attributes=dict.fromkeys(attributes), items=merged_items)
|
| 1259 |
+
|
| 1260 |
+
|
| 1261 |
+
def _infer_return_value(node: ast.FunctionDef, context: EvaluationContext):
|
| 1262 |
+
"""Infer the return value(s) of a function by evaluating all return statements."""
|
| 1263 |
+
return_values = _collect_return_values(node.body, context)
|
| 1264 |
+
|
| 1265 |
+
if not return_values:
|
| 1266 |
+
return None
|
| 1267 |
+
if len(return_values) == 1:
|
| 1268 |
+
return return_values[0]
|
| 1269 |
+
|
| 1270 |
+
policy = get_policy(context)
|
| 1271 |
+
return _merge_values(return_values, policy)
|
| 1272 |
+
|
| 1273 |
+
|
| 1274 |
+
def _collect_return_values(body, context):
|
| 1275 |
+
"""Recursively collect return values from a list of AST statements."""
|
| 1276 |
+
return_values = []
|
| 1277 |
+
for stmt in body:
|
| 1278 |
+
if isinstance(stmt, ast.Return):
|
| 1279 |
+
if stmt.value is None:
|
| 1280 |
+
continue
|
| 1281 |
+
try:
|
| 1282 |
+
value = eval_node(stmt.value, context)
|
| 1283 |
+
if value is not None and value is not NOT_EVALUATED:
|
| 1284 |
+
return_values.append(value)
|
| 1285 |
+
except Exception:
|
| 1286 |
+
pass
|
| 1287 |
+
if isinstance(
|
| 1288 |
+
stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
| 1289 |
+
):
|
| 1290 |
+
continue
|
| 1291 |
+
elif hasattr(stmt, "body") and isinstance(stmt.body, list):
|
| 1292 |
+
return_values.extend(_collect_return_values(stmt.body, context))
|
| 1293 |
+
if isinstance(stmt, ast.Try):
|
| 1294 |
+
for h in stmt.handlers:
|
| 1295 |
+
if hasattr(h, "body"):
|
| 1296 |
+
return_values.extend(_collect_return_values(h.body, context))
|
| 1297 |
+
if hasattr(stmt, "orelse"):
|
| 1298 |
+
return_values.extend(_collect_return_values(stmt.orelse, context))
|
| 1299 |
+
if hasattr(stmt, "finalbody"):
|
| 1300 |
+
return_values.extend(_collect_return_values(stmt.finalbody, context))
|
| 1301 |
+
if hasattr(stmt, "orelse") and isinstance(stmt.orelse, list):
|
| 1302 |
+
return_values.extend(_collect_return_values(stmt.orelse, context))
|
| 1303 |
+
return return_values
|
| 1304 |
+
|
| 1305 |
+
|
| 1306 |
+
def _eval_return_type(func: Callable, node: ast.Call, context: EvaluationContext):
|
| 1307 |
+
"""Evaluate return type of a given callable function.
|
| 1308 |
+
|
| 1309 |
+
Returns the built-in type, a duck or NOT_EVALUATED sentinel.
|
| 1310 |
+
"""
|
| 1311 |
+
try:
|
| 1312 |
+
sig = signature(func)
|
| 1313 |
+
except ValueError:
|
| 1314 |
+
sig = UNKNOWN_SIGNATURE
|
| 1315 |
+
# if annotation was not stringized, or it was stringized
|
| 1316 |
+
# but resolved by signature call we know the return type
|
| 1317 |
+
not_empty = sig.return_annotation is not Signature.empty
|
| 1318 |
+
if not_empty:
|
| 1319 |
+
return _resolve_annotation(sig.return_annotation, context, sig, func, node)
|
| 1320 |
+
return NOT_EVALUATED
|
| 1321 |
+
|
| 1322 |
+
|
| 1323 |
+
def _eval_annotation(
|
| 1324 |
+
annotation: str,
|
| 1325 |
+
context: EvaluationContext,
|
| 1326 |
+
):
|
| 1327 |
+
return (
|
| 1328 |
+
_eval_node_name(annotation, context)
|
| 1329 |
+
if isinstance(annotation, str)
|
| 1330 |
+
else annotation
|
| 1331 |
+
)
|
| 1332 |
+
|
| 1333 |
+
|
| 1334 |
+
class _GetItemDuck(dict):
|
| 1335 |
+
"""A dict subclass that always returns the factory instance and claims to have any item."""
|
| 1336 |
+
|
| 1337 |
+
def __init__(self, factory, *args, **kwargs):
|
| 1338 |
+
super().__init__(*args, **kwargs)
|
| 1339 |
+
self._factory = factory
|
| 1340 |
+
|
| 1341 |
+
def __getitem__(self, key):
|
| 1342 |
+
return self._factory()
|
| 1343 |
+
|
| 1344 |
+
def __contains__(self, key):
|
| 1345 |
+
return True
|
| 1346 |
+
|
| 1347 |
+
|
| 1348 |
+
def _resolve_annotation(
|
| 1349 |
+
annotation: object | str,
|
| 1350 |
+
context: EvaluationContext,
|
| 1351 |
+
sig: Signature | None = None,
|
| 1352 |
+
func: Callable | None = None,
|
| 1353 |
+
node: ast.Call | None = None,
|
| 1354 |
+
):
|
| 1355 |
+
"""Resolve annotation created by user with `typing` module and custom objects."""
|
| 1356 |
+
if annotation is None:
|
| 1357 |
+
return None
|
| 1358 |
+
annotation = _eval_annotation(annotation, context)
|
| 1359 |
+
origin = get_origin(annotation)
|
| 1360 |
+
if annotation is Self and func and hasattr(func, "__self__"):
|
| 1361 |
+
return func.__self__
|
| 1362 |
+
elif origin is Literal:
|
| 1363 |
+
type_args = get_args(annotation)
|
| 1364 |
+
if len(type_args) == 1:
|
| 1365 |
+
return type_args[0]
|
| 1366 |
+
elif annotation is LiteralString:
|
| 1367 |
+
return ""
|
| 1368 |
+
elif annotation is AnyStr:
|
| 1369 |
+
index = None
|
| 1370 |
+
if func and hasattr(func, "__node__"):
|
| 1371 |
+
def_node = func.__node__
|
| 1372 |
+
for i, arg in enumerate(def_node.args.args):
|
| 1373 |
+
if not arg.annotation:
|
| 1374 |
+
continue
|
| 1375 |
+
annotation = _eval_annotation(arg.annotation.id, context)
|
| 1376 |
+
if annotation is AnyStr:
|
| 1377 |
+
index = i
|
| 1378 |
+
break
|
| 1379 |
+
is_bound_method = (
|
| 1380 |
+
isinstance(func, MethodType) and getattr(func, "__self__") is not None
|
| 1381 |
+
)
|
| 1382 |
+
if index and is_bound_method:
|
| 1383 |
+
index -= 1
|
| 1384 |
+
elif sig:
|
| 1385 |
+
for i, (key, value) in enumerate(sig.parameters.items()):
|
| 1386 |
+
if value.annotation is AnyStr:
|
| 1387 |
+
index = i
|
| 1388 |
+
break
|
| 1389 |
+
if index is None:
|
| 1390 |
+
return None
|
| 1391 |
+
if index < 0 or index >= len(node.args):
|
| 1392 |
+
return None
|
| 1393 |
+
return eval_node(node.args[index], context)
|
| 1394 |
+
elif origin is TypeGuard:
|
| 1395 |
+
return False
|
| 1396 |
+
elif origin is set or origin is list:
|
| 1397 |
+
# only one type argument allowed
|
| 1398 |
+
attributes = [
|
| 1399 |
+
attr
|
| 1400 |
+
for attr in dir(
|
| 1401 |
+
_resolve_annotation(get_args(annotation)[0], context, sig, func, node)
|
| 1402 |
+
)
|
| 1403 |
+
]
|
| 1404 |
+
duck = _Duck(attributes=dict.fromkeys(attributes))
|
| 1405 |
+
return _Duck(
|
| 1406 |
+
attributes=dict.fromkeys(dir(origin())),
|
| 1407 |
+
# items are not strrictly needed for set
|
| 1408 |
+
items=_GetItemDuck(lambda: duck),
|
| 1409 |
+
)
|
| 1410 |
+
elif origin is tuple:
|
| 1411 |
+
# multiple type arguments
|
| 1412 |
+
return tuple(
|
| 1413 |
+
_resolve_annotation(arg, context, sig, func, node)
|
| 1414 |
+
for arg in get_args(annotation)
|
| 1415 |
+
)
|
| 1416 |
+
elif origin is Union:
|
| 1417 |
+
# multiple type arguments
|
| 1418 |
+
attributes = [
|
| 1419 |
+
attr
|
| 1420 |
+
for type_arg in get_args(annotation)
|
| 1421 |
+
for attr in dir(_resolve_annotation(type_arg, context, sig, func, node))
|
| 1422 |
+
]
|
| 1423 |
+
return _Duck(attributes=dict.fromkeys(attributes))
|
| 1424 |
+
elif is_typeddict(annotation):
|
| 1425 |
+
return _Duck(
|
| 1426 |
+
attributes=dict.fromkeys(dir(dict())),
|
| 1427 |
+
items={
|
| 1428 |
+
k: _resolve_annotation(v, context, sig, func, node)
|
| 1429 |
+
for k, v in annotation.__annotations__.items()
|
| 1430 |
+
},
|
| 1431 |
+
)
|
| 1432 |
+
elif hasattr(annotation, "_is_protocol"):
|
| 1433 |
+
return _Duck(attributes=dict.fromkeys(dir(annotation)))
|
| 1434 |
+
elif origin is Annotated:
|
| 1435 |
+
type_arg = get_args(annotation)[0]
|
| 1436 |
+
return _resolve_annotation(type_arg, context, sig, func, node)
|
| 1437 |
+
elif isinstance(annotation, NewType):
|
| 1438 |
+
return _eval_or_create_duck(annotation.__supertype__, context)
|
| 1439 |
+
elif isinstance(annotation, TypeAliasType):
|
| 1440 |
+
return _eval_or_create_duck(annotation.__value__, context)
|
| 1441 |
+
else:
|
| 1442 |
+
return _eval_or_create_duck(annotation, context)
|
| 1443 |
+
|
| 1444 |
+
|
| 1445 |
+
def _eval_node_name(node_id: str, context: EvaluationContext):
|
| 1446 |
+
policy = get_policy(context)
|
| 1447 |
+
if node_id in context.transient_locals:
|
| 1448 |
+
return context.transient_locals[node_id]
|
| 1449 |
+
if policy.allow_locals_access and node_id in context.locals:
|
| 1450 |
+
return context.locals[node_id]
|
| 1451 |
+
if policy.allow_globals_access and node_id in context.globals:
|
| 1452 |
+
return context.globals[node_id]
|
| 1453 |
+
if policy.allow_builtins_access and hasattr(builtins, node_id):
|
| 1454 |
+
# note: do not use __builtins__, it is implementation detail of cPython
|
| 1455 |
+
return getattr(builtins, node_id)
|
| 1456 |
+
if policy.allow_auto_import and context.auto_import:
|
| 1457 |
+
return context.auto_import(node_id)
|
| 1458 |
+
if not policy.allow_globals_access and not policy.allow_locals_access:
|
| 1459 |
+
raise GuardRejection(
|
| 1460 |
+
f"Namespace access not allowed in {context.evaluation} mode"
|
| 1461 |
+
)
|
| 1462 |
+
else:
|
| 1463 |
+
raise NameError(f"{node_id} not found in locals, globals, nor builtins")
|
| 1464 |
+
|
| 1465 |
+
|
| 1466 |
+
def _eval_or_create_duck(duck_type, context: EvaluationContext):
|
| 1467 |
+
policy = get_policy(context)
|
| 1468 |
+
# if allow-listed builtin is on type annotation, instantiate it
|
| 1469 |
+
if policy.can_call(duck_type):
|
| 1470 |
+
return duck_type()
|
| 1471 |
+
# if custom class is in type annotation, mock it
|
| 1472 |
+
return _create_duck_for_heap_type(duck_type)
|
| 1473 |
+
|
| 1474 |
+
|
| 1475 |
+
def _create_duck_for_heap_type(duck_type):
|
| 1476 |
+
"""Create an imitation of an object of a given type (a duck).
|
| 1477 |
+
|
| 1478 |
+
Returns the duck or NOT_EVALUATED sentinel if duck could not be created.
|
| 1479 |
+
"""
|
| 1480 |
+
duck = ImpersonatingDuck()
|
| 1481 |
+
try:
|
| 1482 |
+
# this only works for heap types, not builtins
|
| 1483 |
+
duck.__class__ = duck_type
|
| 1484 |
+
return duck
|
| 1485 |
+
except TypeError:
|
| 1486 |
+
pass
|
| 1487 |
+
return NOT_EVALUATED
|
| 1488 |
+
|
| 1489 |
+
|
| 1490 |
+
SUPPORTED_EXTERNAL_GETITEM = {
|
| 1491 |
+
("pandas", "core", "indexing", "_iLocIndexer"),
|
| 1492 |
+
("pandas", "core", "indexing", "_LocIndexer"),
|
| 1493 |
+
("pandas", "DataFrame"),
|
| 1494 |
+
("pandas", "Series"),
|
| 1495 |
+
("numpy", "ndarray"),
|
| 1496 |
+
("numpy", "void"),
|
| 1497 |
+
}
|
| 1498 |
+
|
| 1499 |
+
|
| 1500 |
+
BUILTIN_GETITEM: set[InstancesHaveGetItem] = {
|
| 1501 |
+
dict,
|
| 1502 |
+
str, # type: ignore[arg-type]
|
| 1503 |
+
bytes, # type: ignore[arg-type]
|
| 1504 |
+
list,
|
| 1505 |
+
tuple,
|
| 1506 |
+
type, # for type annotations like list[str]
|
| 1507 |
+
_Duck,
|
| 1508 |
+
collections.defaultdict,
|
| 1509 |
+
collections.deque,
|
| 1510 |
+
collections.OrderedDict,
|
| 1511 |
+
collections.ChainMap,
|
| 1512 |
+
collections.UserDict,
|
| 1513 |
+
collections.UserList,
|
| 1514 |
+
collections.UserString, # type: ignore[arg-type]
|
| 1515 |
+
_DummyNamedTuple,
|
| 1516 |
+
_IdentitySubscript,
|
| 1517 |
+
}
|
| 1518 |
+
|
| 1519 |
+
|
| 1520 |
+
def _list_methods(cls, source=None):
|
| 1521 |
+
"""For use on immutable objects or with methods returning a copy"""
|
| 1522 |
+
return [getattr(cls, k) for k in (source if source else dir(cls))]
|
| 1523 |
+
|
| 1524 |
+
|
| 1525 |
+
dict_non_mutating_methods = ("copy", "keys", "values", "items")
|
| 1526 |
+
list_non_mutating_methods = ("copy", "index", "count")
|
| 1527 |
+
set_non_mutating_methods = set(dir(set)) & set(dir(frozenset))
|
| 1528 |
+
|
| 1529 |
+
|
| 1530 |
+
dict_keys: type[collections.abc.KeysView] = type({}.keys())
|
| 1531 |
+
dict_values: type = type({}.values())
|
| 1532 |
+
dict_items: type = type({}.items())
|
| 1533 |
+
|
| 1534 |
+
NUMERICS = {int, float, complex}
|
| 1535 |
+
|
| 1536 |
+
ALLOWED_CALLS = {
|
| 1537 |
+
bytes,
|
| 1538 |
+
*_list_methods(bytes),
|
| 1539 |
+
bytes.__iter__,
|
| 1540 |
+
dict,
|
| 1541 |
+
*_list_methods(dict, dict_non_mutating_methods),
|
| 1542 |
+
dict.__iter__,
|
| 1543 |
+
dict_keys.__iter__,
|
| 1544 |
+
dict_values.__iter__,
|
| 1545 |
+
dict_items.__iter__,
|
| 1546 |
+
dict_keys.isdisjoint,
|
| 1547 |
+
list,
|
| 1548 |
+
*_list_methods(list, list_non_mutating_methods),
|
| 1549 |
+
list.__iter__,
|
| 1550 |
+
set,
|
| 1551 |
+
*_list_methods(set, set_non_mutating_methods),
|
| 1552 |
+
set.__iter__,
|
| 1553 |
+
frozenset,
|
| 1554 |
+
*_list_methods(frozenset),
|
| 1555 |
+
frozenset.__iter__,
|
| 1556 |
+
range,
|
| 1557 |
+
range.__iter__,
|
| 1558 |
+
str,
|
| 1559 |
+
*_list_methods(str),
|
| 1560 |
+
str.__iter__,
|
| 1561 |
+
tuple,
|
| 1562 |
+
*_list_methods(tuple),
|
| 1563 |
+
tuple.__iter__,
|
| 1564 |
+
bool,
|
| 1565 |
+
*_list_methods(bool),
|
| 1566 |
+
*NUMERICS,
|
| 1567 |
+
*[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)],
|
| 1568 |
+
collections.deque,
|
| 1569 |
+
*_list_methods(collections.deque, list_non_mutating_methods),
|
| 1570 |
+
collections.deque.__iter__,
|
| 1571 |
+
collections.defaultdict,
|
| 1572 |
+
*_list_methods(collections.defaultdict, dict_non_mutating_methods),
|
| 1573 |
+
collections.defaultdict.__iter__,
|
| 1574 |
+
collections.OrderedDict,
|
| 1575 |
+
*_list_methods(collections.OrderedDict, dict_non_mutating_methods),
|
| 1576 |
+
collections.OrderedDict.__iter__,
|
| 1577 |
+
collections.UserDict,
|
| 1578 |
+
*_list_methods(collections.UserDict, dict_non_mutating_methods),
|
| 1579 |
+
collections.UserDict.__iter__,
|
| 1580 |
+
collections.UserList,
|
| 1581 |
+
*_list_methods(collections.UserList, list_non_mutating_methods),
|
| 1582 |
+
collections.UserList.__iter__,
|
| 1583 |
+
collections.UserString,
|
| 1584 |
+
*_list_methods(collections.UserString, dir(str)),
|
| 1585 |
+
collections.UserString.__iter__,
|
| 1586 |
+
collections.Counter,
|
| 1587 |
+
*_list_methods(collections.Counter, dict_non_mutating_methods),
|
| 1588 |
+
collections.Counter.__iter__,
|
| 1589 |
+
collections.Counter.elements,
|
| 1590 |
+
collections.Counter.most_common,
|
| 1591 |
+
object.__dir__,
|
| 1592 |
+
type.__dir__,
|
| 1593 |
+
_Duck.__dir__,
|
| 1594 |
+
}
|
| 1595 |
+
|
| 1596 |
+
BUILTIN_GETATTR: set[MayHaveGetattr] = {
|
| 1597 |
+
*BUILTIN_GETITEM,
|
| 1598 |
+
set,
|
| 1599 |
+
frozenset,
|
| 1600 |
+
object,
|
| 1601 |
+
type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`.
|
| 1602 |
+
*NUMERICS,
|
| 1603 |
+
dict_keys,
|
| 1604 |
+
MethodDescriptorType,
|
| 1605 |
+
ModuleType,
|
| 1606 |
+
}
|
| 1607 |
+
|
| 1608 |
+
|
| 1609 |
+
BUILTIN_OPERATIONS = {*BUILTIN_GETATTR}
|
| 1610 |
+
|
| 1611 |
+
EVALUATION_POLICIES = {
|
| 1612 |
+
"minimal": EvaluationPolicy(
|
| 1613 |
+
allow_builtins_access=True,
|
| 1614 |
+
allow_locals_access=False,
|
| 1615 |
+
allow_globals_access=False,
|
| 1616 |
+
allow_item_access=False,
|
| 1617 |
+
allow_attr_access=False,
|
| 1618 |
+
allowed_calls=set(),
|
| 1619 |
+
allow_any_calls=False,
|
| 1620 |
+
allow_all_operations=False,
|
| 1621 |
+
),
|
| 1622 |
+
"limited": SelectivePolicy(
|
| 1623 |
+
allowed_getitem=BUILTIN_GETITEM,
|
| 1624 |
+
allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM,
|
| 1625 |
+
allowed_getattr=BUILTIN_GETATTR,
|
| 1626 |
+
allowed_getattr_external={
|
| 1627 |
+
# pandas Series/Frame implements custom `__getattr__`
|
| 1628 |
+
("pandas", "DataFrame"),
|
| 1629 |
+
("pandas", "Series"),
|
| 1630 |
+
},
|
| 1631 |
+
allowed_operations=BUILTIN_OPERATIONS,
|
| 1632 |
+
allow_builtins_access=True,
|
| 1633 |
+
allow_locals_access=True,
|
| 1634 |
+
allow_globals_access=True,
|
| 1635 |
+
allow_getitem_on_types=True,
|
| 1636 |
+
allowed_calls=ALLOWED_CALLS,
|
| 1637 |
+
),
|
| 1638 |
+
"unsafe": EvaluationPolicy(
|
| 1639 |
+
allow_builtins_access=True,
|
| 1640 |
+
allow_locals_access=True,
|
| 1641 |
+
allow_globals_access=True,
|
| 1642 |
+
allow_attr_access=True,
|
| 1643 |
+
allow_item_access=True,
|
| 1644 |
+
allow_any_calls=True,
|
| 1645 |
+
allow_all_operations=True,
|
| 1646 |
+
),
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
+
|
| 1650 |
+
__all__ = [
|
| 1651 |
+
"guarded_eval",
|
| 1652 |
+
"eval_node",
|
| 1653 |
+
"GuardRejection",
|
| 1654 |
+
"EvaluationContext",
|
| 1655 |
+
"_unbind_method",
|
| 1656 |
+
]
|
lib/python3.12/site-packages/IPython/core/magic.py
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
"""Magic functions for InteractiveShell."""
|
| 4 |
+
|
| 5 |
+
# -----------------------------------------------------------------------------
|
| 6 |
+
# Copyright (C) 2001 Janko Hauser <jhauser@zscout.de> and
|
| 7 |
+
# Copyright (C) 2001 Fernando Perez <fperez@colorado.edu>
|
| 8 |
+
# Copyright (C) 2008 The IPython Development Team
|
| 9 |
+
|
| 10 |
+
# Distributed under the terms of the BSD License. The full license is in
|
| 11 |
+
# the file COPYING, distributed as part of this software.
|
| 12 |
+
# -----------------------------------------------------------------------------
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import re
|
| 16 |
+
import sys
|
| 17 |
+
from getopt import getopt, GetoptError
|
| 18 |
+
|
| 19 |
+
from traitlets.config.configurable import Configurable
|
| 20 |
+
from . import oinspect
|
| 21 |
+
from .error import UsageError
|
| 22 |
+
from .inputtransformer2 import ESC_MAGIC, ESC_MAGIC2
|
| 23 |
+
from ..utils.ipstruct import Struct
|
| 24 |
+
from ..utils.process import arg_split
|
| 25 |
+
from ..utils.text import dedent
|
| 26 |
+
from traitlets import Bool, Dict, Instance, observe
|
| 27 |
+
from logging import error
|
| 28 |
+
|
| 29 |
+
import typing as t
|
| 30 |
+
|
| 31 |
+
if t.TYPE_CHECKING:
|
| 32 |
+
from IPython.core.interactiveshell import InteractiveShell
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# -----------------------------------------------------------------------------
|
| 36 |
+
# Globals
|
| 37 |
+
# -----------------------------------------------------------------------------
|
| 38 |
+
|
| 39 |
+
# A dict we'll use for each class that has magics, used as temporary storage to
|
| 40 |
+
# pass information between the @line/cell_magic method decorators and the
|
| 41 |
+
# @magics_class class decorator, because the method decorators have no
|
| 42 |
+
# access to the class when they run. See for more details:
|
| 43 |
+
# http://stackoverflow.com/questions/2366713/can-a-python-decorator-of-an-instance-method-access-the-class
|
| 44 |
+
|
| 45 |
+
magics: t.Dict = dict(line={}, cell={})
|
| 46 |
+
|
| 47 |
+
magic_kinds = ("line", "cell")
|
| 48 |
+
magic_spec = ("line", "cell", "line_cell")
|
| 49 |
+
magic_escapes = dict(line=ESC_MAGIC, cell=ESC_MAGIC2)
|
| 50 |
+
|
| 51 |
+
# -----------------------------------------------------------------------------
|
| 52 |
+
# Utility classes and functions
|
| 53 |
+
# -----------------------------------------------------------------------------
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class Bunch:
|
| 57 |
+
pass
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def on_off(tag):
|
| 61 |
+
"""Return an ON/OFF string for a 1/0 input. Simple utility function."""
|
| 62 |
+
return ["OFF", "ON"][tag]
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def compress_dhist(dh):
|
| 66 |
+
"""Compress a directory history into a new one with at most 20 entries.
|
| 67 |
+
|
| 68 |
+
Return a new list made from the first and last 10 elements of dhist after
|
| 69 |
+
removal of duplicates.
|
| 70 |
+
"""
|
| 71 |
+
head, tail = dh[:-10], dh[-10:]
|
| 72 |
+
|
| 73 |
+
newhead = []
|
| 74 |
+
done = set()
|
| 75 |
+
for h in head:
|
| 76 |
+
if h in done:
|
| 77 |
+
continue
|
| 78 |
+
newhead.append(h)
|
| 79 |
+
done.add(h)
|
| 80 |
+
|
| 81 |
+
return newhead + tail
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def needs_local_scope(func):
|
| 85 |
+
"""Decorator to mark magic functions which need to local scope to run."""
|
| 86 |
+
func.needs_local_scope = True
|
| 87 |
+
return func
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# -----------------------------------------------------------------------------
|
| 91 |
+
# Class and method decorators for registering magics
|
| 92 |
+
# -----------------------------------------------------------------------------
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def magics_class(cls):
|
| 96 |
+
"""Class decorator for all subclasses of the main Magics class.
|
| 97 |
+
|
| 98 |
+
Any class that subclasses Magics *must* also apply this decorator, to
|
| 99 |
+
ensure that all the methods that have been decorated as line/cell magics
|
| 100 |
+
get correctly registered in the class instance. This is necessary because
|
| 101 |
+
when method decorators run, the class does not exist yet, so they
|
| 102 |
+
temporarily store their information into a module global. Application of
|
| 103 |
+
this class decorator copies that global data to the class instance and
|
| 104 |
+
clears the global.
|
| 105 |
+
|
| 106 |
+
Obviously, this mechanism is not thread-safe, which means that the
|
| 107 |
+
*creation* of subclasses of Magic should only be done in a single-thread
|
| 108 |
+
context. Instantiation of the classes has no restrictions. Given that
|
| 109 |
+
these classes are typically created at IPython startup time and before user
|
| 110 |
+
application code becomes active, in practice this should not pose any
|
| 111 |
+
problems.
|
| 112 |
+
"""
|
| 113 |
+
cls.registered = True
|
| 114 |
+
cls.magics = dict(line=magics["line"], cell=magics["cell"])
|
| 115 |
+
magics["line"] = {}
|
| 116 |
+
magics["cell"] = {}
|
| 117 |
+
return cls
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def record_magic(dct, magic_kind, magic_name, func):
|
| 121 |
+
"""Utility function to store a function as a magic of a specific kind.
|
| 122 |
+
|
| 123 |
+
Parameters
|
| 124 |
+
----------
|
| 125 |
+
dct : dict
|
| 126 |
+
A dictionary with 'line' and 'cell' subdicts.
|
| 127 |
+
magic_kind : str
|
| 128 |
+
Kind of magic to be stored.
|
| 129 |
+
magic_name : str
|
| 130 |
+
Key to store the magic as.
|
| 131 |
+
func : function
|
| 132 |
+
Callable object to store.
|
| 133 |
+
"""
|
| 134 |
+
if magic_kind == "line_cell":
|
| 135 |
+
dct["line"][magic_name] = dct["cell"][magic_name] = func
|
| 136 |
+
else:
|
| 137 |
+
dct[magic_kind][magic_name] = func
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def validate_type(magic_kind):
|
| 141 |
+
"""Ensure that the given magic_kind is valid.
|
| 142 |
+
|
| 143 |
+
Check that the given magic_kind is one of the accepted spec types (stored
|
| 144 |
+
in the global `magic_spec`), raise ValueError otherwise.
|
| 145 |
+
"""
|
| 146 |
+
if magic_kind not in magic_spec:
|
| 147 |
+
raise ValueError(
|
| 148 |
+
"magic_kind must be one of %s, %s given" % magic_kinds, magic_kind
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# The docstrings for the decorator below will be fairly similar for the two
|
| 153 |
+
# types (method and function), so we generate them here once and reuse the
|
| 154 |
+
# templates below.
|
| 155 |
+
_docstring_template = """Decorate the given {0} as {1} magic.
|
| 156 |
+
|
| 157 |
+
The decorator can be used with or without arguments, as follows.
|
| 158 |
+
|
| 159 |
+
i) without arguments: it will create a {1} magic named as the {0} being
|
| 160 |
+
decorated::
|
| 161 |
+
|
| 162 |
+
@deco
|
| 163 |
+
def foo(...)
|
| 164 |
+
|
| 165 |
+
will create a {1} magic named `foo`.
|
| 166 |
+
|
| 167 |
+
ii) with one string argument: which will be used as the actual name of the
|
| 168 |
+
resulting magic::
|
| 169 |
+
|
| 170 |
+
@deco('bar')
|
| 171 |
+
def foo(...)
|
| 172 |
+
|
| 173 |
+
will create a {1} magic named `bar`.
|
| 174 |
+
|
| 175 |
+
To register a class magic use ``Interactiveshell.register_magic(class or instance)``.
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
# These two are decorator factories. While they are conceptually very similar,
|
| 179 |
+
# there are enough differences in the details that it's simpler to have them
|
| 180 |
+
# written as completely standalone functions rather than trying to share code
|
| 181 |
+
# and make a single one with convoluted logic.
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def _method_magic_marker(magic_kind):
|
| 185 |
+
"""Decorator factory for methods in Magics subclasses."""
|
| 186 |
+
|
| 187 |
+
validate_type(magic_kind)
|
| 188 |
+
|
| 189 |
+
# This is a closure to capture the magic_kind. We could also use a class,
|
| 190 |
+
# but it's overkill for just that one bit of state.
|
| 191 |
+
def magic_deco(arg):
|
| 192 |
+
if callable(arg):
|
| 193 |
+
# "Naked" decorator call (just @foo, no args)
|
| 194 |
+
func = arg
|
| 195 |
+
name = func.__name__
|
| 196 |
+
retval = arg
|
| 197 |
+
record_magic(magics, magic_kind, name, name)
|
| 198 |
+
elif isinstance(arg, str):
|
| 199 |
+
# Decorator called with arguments (@foo('bar'))
|
| 200 |
+
name = arg
|
| 201 |
+
|
| 202 |
+
def mark(func, *a, **kw):
|
| 203 |
+
record_magic(magics, magic_kind, name, func.__name__)
|
| 204 |
+
return func
|
| 205 |
+
|
| 206 |
+
retval = mark
|
| 207 |
+
else:
|
| 208 |
+
raise TypeError("Decorator can only be called with string or function")
|
| 209 |
+
return retval
|
| 210 |
+
|
| 211 |
+
# Ensure the resulting decorator has a usable docstring
|
| 212 |
+
magic_deco.__doc__ = _docstring_template.format("method", magic_kind)
|
| 213 |
+
return magic_deco
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def _function_magic_marker(magic_kind):
|
| 217 |
+
"""Decorator factory for standalone functions."""
|
| 218 |
+
validate_type(magic_kind)
|
| 219 |
+
|
| 220 |
+
# This is a closure to capture the magic_kind. We could also use a class,
|
| 221 |
+
# but it's overkill for just that one bit of state.
|
| 222 |
+
def magic_deco(arg):
|
| 223 |
+
# Find get_ipython() in the caller's namespace
|
| 224 |
+
caller = sys._getframe(1)
|
| 225 |
+
for ns in ["f_locals", "f_globals", "f_builtins"]:
|
| 226 |
+
get_ipython = getattr(caller, ns).get("get_ipython")
|
| 227 |
+
if get_ipython is not None:
|
| 228 |
+
break
|
| 229 |
+
else:
|
| 230 |
+
raise NameError(
|
| 231 |
+
"Decorator can only run in context where `get_ipython` exists"
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
ip = get_ipython()
|
| 235 |
+
|
| 236 |
+
if callable(arg):
|
| 237 |
+
# "Naked" decorator call (just @foo, no args)
|
| 238 |
+
func = arg
|
| 239 |
+
name = func.__name__
|
| 240 |
+
ip.register_magic_function(func, magic_kind, name)
|
| 241 |
+
retval = arg
|
| 242 |
+
elif isinstance(arg, str):
|
| 243 |
+
# Decorator called with arguments (@foo('bar'))
|
| 244 |
+
name = arg
|
| 245 |
+
|
| 246 |
+
def mark(func, *a, **kw):
|
| 247 |
+
ip.register_magic_function(func, magic_kind, name)
|
| 248 |
+
return func
|
| 249 |
+
|
| 250 |
+
retval = mark
|
| 251 |
+
else:
|
| 252 |
+
raise TypeError("Decorator can only be called with string or function")
|
| 253 |
+
return retval
|
| 254 |
+
|
| 255 |
+
# Ensure the resulting decorator has a usable docstring
|
| 256 |
+
ds = _docstring_template.format("function", magic_kind)
|
| 257 |
+
|
| 258 |
+
ds += dedent(
|
| 259 |
+
"""
|
| 260 |
+
Note: this decorator can only be used in a context where IPython is already
|
| 261 |
+
active, so that the `get_ipython()` call succeeds. You can therefore use
|
| 262 |
+
it in your startup files loaded after IPython initializes, but *not* in the
|
| 263 |
+
IPython configuration file itself, which is executed before IPython is
|
| 264 |
+
fully up and running. Any file located in the `startup` subdirectory of
|
| 265 |
+
your configuration profile will be OK in this sense.
|
| 266 |
+
"""
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
magic_deco.__doc__ = ds
|
| 270 |
+
return magic_deco
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
MAGIC_NO_VAR_EXPAND_ATTR = "_ipython_magic_no_var_expand"
|
| 274 |
+
MAGIC_OUTPUT_CAN_BE_SILENCED = "_ipython_magic_output_can_be_silenced"
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def no_var_expand(magic_func):
|
| 278 |
+
"""Mark a magic function as not needing variable expansion
|
| 279 |
+
|
| 280 |
+
By default, IPython interprets `{a}` or `$a` in the line passed to magics
|
| 281 |
+
as variables that should be interpolated from the interactive namespace
|
| 282 |
+
before passing the line to the magic function.
|
| 283 |
+
This is not always desirable, e.g. when the magic executes Python code
|
| 284 |
+
(%timeit, %time, etc.).
|
| 285 |
+
Decorate magics with `@no_var_expand` to opt-out of variable expansion.
|
| 286 |
+
|
| 287 |
+
.. versionadded:: 7.3
|
| 288 |
+
"""
|
| 289 |
+
setattr(magic_func, MAGIC_NO_VAR_EXPAND_ATTR, True)
|
| 290 |
+
return magic_func
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def output_can_be_silenced(magic_func):
|
| 294 |
+
"""Mark a magic function so its output may be silenced.
|
| 295 |
+
|
| 296 |
+
The output is silenced if the Python code used as a parameter of
|
| 297 |
+
the magic ends in a semicolon, not counting a Python comment that can
|
| 298 |
+
follow it.
|
| 299 |
+
"""
|
| 300 |
+
setattr(magic_func, MAGIC_OUTPUT_CAN_BE_SILENCED, True)
|
| 301 |
+
return magic_func
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
# Create the actual decorators for public use
|
| 305 |
+
|
| 306 |
+
# These three are used to decorate methods in class definitions
|
| 307 |
+
line_magic = _method_magic_marker("line")
|
| 308 |
+
cell_magic = _method_magic_marker("cell")
|
| 309 |
+
line_cell_magic = _method_magic_marker("line_cell")
|
| 310 |
+
|
| 311 |
+
# These three decorate standalone functions and perform the decoration
|
| 312 |
+
# immediately. They can only run where get_ipython() works
|
| 313 |
+
register_line_magic = _function_magic_marker("line")
|
| 314 |
+
register_cell_magic = _function_magic_marker("cell")
|
| 315 |
+
register_line_cell_magic = _function_magic_marker("line_cell")
|
| 316 |
+
|
| 317 |
+
# -----------------------------------------------------------------------------
|
| 318 |
+
# Core Magic classes
|
| 319 |
+
# -----------------------------------------------------------------------------
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
class MagicsManager(Configurable):
|
| 323 |
+
"""Object that handles all magic-related functionality for IPython."""
|
| 324 |
+
|
| 325 |
+
# Non-configurable class attributes
|
| 326 |
+
|
| 327 |
+
# A two-level dict, first keyed by magic type, then by magic function, and
|
| 328 |
+
# holding the actual callable object as value. This is the dict used for
|
| 329 |
+
# magic function dispatch
|
| 330 |
+
magics = Dict()
|
| 331 |
+
lazy_magics = Dict(
|
| 332 |
+
help="""
|
| 333 |
+
Mapping from magic names to modules to load.
|
| 334 |
+
|
| 335 |
+
This can be used in IPython/IPykernel configuration to declare lazy magics
|
| 336 |
+
that will only be imported/registered on first use.
|
| 337 |
+
|
| 338 |
+
For example::
|
| 339 |
+
|
| 340 |
+
c.MagicsManager.lazy_magics = {
|
| 341 |
+
"my_magic": "slow.to.import",
|
| 342 |
+
"my_other_magic": "also.slow",
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
On first invocation of `%my_magic`, `%%my_magic`, `%%my_other_magic` or
|
| 346 |
+
`%%my_other_magic`, the corresponding module will be loaded as an ipython
|
| 347 |
+
extensions as if you had previously done `%load_ext ipython`.
|
| 348 |
+
|
| 349 |
+
Magics names should be without percent(s) as magics can be both cell
|
| 350 |
+
and line magics.
|
| 351 |
+
|
| 352 |
+
Lazy loading happen relatively late in execution process, and
|
| 353 |
+
complex extensions that manipulate Python/IPython internal state or global state
|
| 354 |
+
might not support lazy loading.
|
| 355 |
+
"""
|
| 356 |
+
).tag(
|
| 357 |
+
config=True,
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
# A registry of the original objects that we've been given holding magics.
|
| 361 |
+
registry = Dict()
|
| 362 |
+
|
| 363 |
+
shell = Instance(
|
| 364 |
+
"IPython.core.interactiveshell.InteractiveShellABC", allow_none=True
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
auto_magic = Bool(
|
| 368 |
+
True, help="Automatically call line magics without requiring explicit % prefix"
|
| 369 |
+
).tag(config=True)
|
| 370 |
+
|
| 371 |
+
@observe("auto_magic")
|
| 372 |
+
def _auto_magic_changed(self, change):
|
| 373 |
+
assert self.shell is not None
|
| 374 |
+
self.shell.automagic = change["new"]
|
| 375 |
+
|
| 376 |
+
_auto_status = [
|
| 377 |
+
"Automagic is OFF, % prefix IS needed for line magics.",
|
| 378 |
+
"Automagic is ON, % prefix IS NOT needed for line magics.",
|
| 379 |
+
]
|
| 380 |
+
|
| 381 |
+
user_magics = Instance("IPython.core.magics.UserMagics", allow_none=True)
|
| 382 |
+
|
| 383 |
+
def __init__(self, shell=None, config=None, user_magics=None, **traits):
|
| 384 |
+
super(MagicsManager, self).__init__(
|
| 385 |
+
shell=shell, config=config, user_magics=user_magics, **traits
|
| 386 |
+
)
|
| 387 |
+
self.magics = dict(line={}, cell={})
|
| 388 |
+
# Let's add the user_magics to the registry for uniformity, so *all*
|
| 389 |
+
# registered magic containers can be found there.
|
| 390 |
+
self.registry[user_magics.__class__.__name__] = user_magics
|
| 391 |
+
|
| 392 |
+
def auto_status(self):
|
| 393 |
+
"""Return descriptive string with automagic status."""
|
| 394 |
+
return self._auto_status[self.auto_magic]
|
| 395 |
+
|
| 396 |
+
def lsmagic(self):
|
| 397 |
+
"""Return a dict of currently available magic functions.
|
| 398 |
+
|
| 399 |
+
The return dict has the keys 'line' and 'cell', corresponding to the
|
| 400 |
+
two types of magics we support. Each value is a list of names.
|
| 401 |
+
"""
|
| 402 |
+
return self.magics
|
| 403 |
+
|
| 404 |
+
def lsmagic_docs(self, brief=False, missing=""):
|
| 405 |
+
"""Return dict of documentation of magic functions.
|
| 406 |
+
|
| 407 |
+
The return dict has the keys 'line' and 'cell', corresponding to the
|
| 408 |
+
two types of magics we support. Each value is a dict keyed by magic
|
| 409 |
+
name whose value is the function docstring. If a docstring is
|
| 410 |
+
unavailable, the value of `missing` is used instead.
|
| 411 |
+
|
| 412 |
+
If brief is True, only the first line of each docstring will be returned.
|
| 413 |
+
"""
|
| 414 |
+
docs = {}
|
| 415 |
+
for m_type in self.magics:
|
| 416 |
+
m_docs = {}
|
| 417 |
+
for m_name, m_func in self.magics[m_type].items():
|
| 418 |
+
if m_func.__doc__:
|
| 419 |
+
if brief:
|
| 420 |
+
m_docs[m_name] = m_func.__doc__.split("\n", 1)[0]
|
| 421 |
+
else:
|
| 422 |
+
m_docs[m_name] = m_func.__doc__.rstrip()
|
| 423 |
+
else:
|
| 424 |
+
m_docs[m_name] = missing
|
| 425 |
+
docs[m_type] = m_docs
|
| 426 |
+
return docs
|
| 427 |
+
|
| 428 |
+
def register_lazy(self, name: str, fully_qualified_name: str) -> None:
|
| 429 |
+
"""
|
| 430 |
+
Lazily register a magic via an extension.
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
Parameters
|
| 434 |
+
----------
|
| 435 |
+
name : str
|
| 436 |
+
Name of the magic you wish to register.
|
| 437 |
+
fully_qualified_name :
|
| 438 |
+
Fully qualified name of the module/submodule that should be loaded
|
| 439 |
+
as an extensions when the magic is first called.
|
| 440 |
+
It is assumed that loading this extensions will register the given
|
| 441 |
+
magic.
|
| 442 |
+
"""
|
| 443 |
+
|
| 444 |
+
self.lazy_magics[name] = fully_qualified_name
|
| 445 |
+
|
| 446 |
+
def register(self, *magic_objects):
|
| 447 |
+
"""Register one or more instances of Magics.
|
| 448 |
+
|
| 449 |
+
Take one or more classes or instances of classes that subclass the main
|
| 450 |
+
`core.Magic` class, and register them with IPython to use the magic
|
| 451 |
+
functions they provide. The registration process will then ensure that
|
| 452 |
+
any methods that have decorated to provide line and/or cell magics will
|
| 453 |
+
be recognized with the `%x`/`%%x` syntax as a line/cell magic
|
| 454 |
+
respectively.
|
| 455 |
+
|
| 456 |
+
If classes are given, they will be instantiated with the default
|
| 457 |
+
constructor. If your classes need a custom constructor, you should
|
| 458 |
+
instanitate them first and pass the instance.
|
| 459 |
+
|
| 460 |
+
The provided arguments can be an arbitrary mix of classes and instances.
|
| 461 |
+
|
| 462 |
+
Parameters
|
| 463 |
+
----------
|
| 464 |
+
*magic_objects : one or more classes or instances
|
| 465 |
+
"""
|
| 466 |
+
# Start by validating them to ensure they have all had their magic
|
| 467 |
+
# methods registered at the instance level
|
| 468 |
+
for m in magic_objects:
|
| 469 |
+
if not m.registered:
|
| 470 |
+
raise ValueError(
|
| 471 |
+
"Class of magics %r was constructed without "
|
| 472 |
+
"the @register_magics class decorator"
|
| 473 |
+
)
|
| 474 |
+
if isinstance(m, type):
|
| 475 |
+
# If we're given an uninstantiated class
|
| 476 |
+
m = m(shell=self.shell)
|
| 477 |
+
|
| 478 |
+
# Now that we have an instance, we can register it and update the
|
| 479 |
+
# table of callables
|
| 480 |
+
self.registry[m.__class__.__name__] = m
|
| 481 |
+
for mtype in magic_kinds:
|
| 482 |
+
self.magics[mtype].update(m.magics[mtype])
|
| 483 |
+
|
| 484 |
+
def register_function(self, func, magic_kind="line", magic_name=None):
|
| 485 |
+
"""Expose a standalone function as magic function for IPython.
|
| 486 |
+
|
| 487 |
+
This will create an IPython magic (line, cell or both) from a
|
| 488 |
+
standalone function. The functions should have the following
|
| 489 |
+
signatures:
|
| 490 |
+
|
| 491 |
+
* For line magics: `def f(line)`
|
| 492 |
+
* For cell magics: `def f(line, cell)`
|
| 493 |
+
* For a function that does both: `def f(line, cell=None)`
|
| 494 |
+
|
| 495 |
+
In the latter case, the function will be called with `cell==None` when
|
| 496 |
+
invoked as `%f`, and with cell as a string when invoked as `%%f`.
|
| 497 |
+
|
| 498 |
+
Parameters
|
| 499 |
+
----------
|
| 500 |
+
func : callable
|
| 501 |
+
Function to be registered as a magic.
|
| 502 |
+
magic_kind : str
|
| 503 |
+
Kind of magic, one of 'line', 'cell' or 'line_cell'
|
| 504 |
+
magic_name : optional str
|
| 505 |
+
If given, the name the magic will have in the IPython namespace. By
|
| 506 |
+
default, the name of the function itself is used.
|
| 507 |
+
"""
|
| 508 |
+
|
| 509 |
+
# Create the new method in the user_magics and register it in the
|
| 510 |
+
# global table
|
| 511 |
+
validate_type(magic_kind)
|
| 512 |
+
magic_name = func.__name__ if magic_name is None else magic_name
|
| 513 |
+
setattr(self.user_magics, magic_name, func)
|
| 514 |
+
record_magic(self.magics, magic_kind, magic_name, func)
|
| 515 |
+
|
| 516 |
+
def register_alias(
|
| 517 |
+
self, alias_name, magic_name, magic_kind="line", magic_params=None
|
| 518 |
+
):
|
| 519 |
+
"""Register an alias to a magic function.
|
| 520 |
+
|
| 521 |
+
The alias is an instance of :class:`MagicAlias`, which holds the
|
| 522 |
+
name and kind of the magic it should call. Binding is done at
|
| 523 |
+
call time, so if the underlying magic function is changed the alias
|
| 524 |
+
will call the new function.
|
| 525 |
+
|
| 526 |
+
Parameters
|
| 527 |
+
----------
|
| 528 |
+
alias_name : str
|
| 529 |
+
The name of the magic to be registered.
|
| 530 |
+
magic_name : str
|
| 531 |
+
The name of an existing magic.
|
| 532 |
+
magic_kind : str
|
| 533 |
+
Kind of magic, one of 'line' or 'cell'
|
| 534 |
+
"""
|
| 535 |
+
|
| 536 |
+
# `validate_type` is too permissive, as it allows 'line_cell'
|
| 537 |
+
# which we do not handle.
|
| 538 |
+
if magic_kind not in magic_kinds:
|
| 539 |
+
raise ValueError(
|
| 540 |
+
"magic_kind must be one of %s, %s given" % magic_kinds, magic_kind
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
alias = MagicAlias(self.shell, magic_name, magic_kind, magic_params)
|
| 544 |
+
setattr(self.user_magics, alias_name, alias)
|
| 545 |
+
record_magic(self.magics, magic_kind, alias_name, alias)
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
# Key base class that provides the central functionality for magics.
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
class Magics(Configurable):
|
| 552 |
+
"""Base class for implementing magic functions.
|
| 553 |
+
|
| 554 |
+
Shell functions which can be reached as %function_name. All magic
|
| 555 |
+
functions should accept a string, which they can parse for their own
|
| 556 |
+
needs. This can make some functions easier to type, eg `%cd ../`
|
| 557 |
+
vs. `%cd("../")`
|
| 558 |
+
|
| 559 |
+
Classes providing magic functions need to subclass this class, and they
|
| 560 |
+
MUST:
|
| 561 |
+
|
| 562 |
+
- Use the method decorators `@line_magic` and `@cell_magic` to decorate
|
| 563 |
+
individual methods as magic functions, AND
|
| 564 |
+
|
| 565 |
+
- Use the class decorator `@magics_class` to ensure that the magic
|
| 566 |
+
methods are properly registered at the instance level upon instance
|
| 567 |
+
initialization.
|
| 568 |
+
|
| 569 |
+
See :mod:`magic_functions` for examples of actual implementation classes.
|
| 570 |
+
"""
|
| 571 |
+
|
| 572 |
+
# Dict holding all command-line options for each magic.
|
| 573 |
+
options_table: dict[str, t.Any] = {}
|
| 574 |
+
# Dict for the mapping of magic names to methods, set by class decorator
|
| 575 |
+
magics: dict[str, t.Any] = {}
|
| 576 |
+
# Flag to check that the class decorator was properly applied
|
| 577 |
+
registered: bool = False
|
| 578 |
+
# Instance of IPython shell
|
| 579 |
+
shell: None | InteractiveShell = None
|
| 580 |
+
|
| 581 |
+
def __init__(self, shell=None, **kwargs):
|
| 582 |
+
if not (self.__class__.registered):
|
| 583 |
+
raise ValueError(
|
| 584 |
+
"Magics subclass without registration - "
|
| 585 |
+
"did you forget to apply @magics_class?"
|
| 586 |
+
)
|
| 587 |
+
if shell is not None:
|
| 588 |
+
if hasattr(shell, "configurables"):
|
| 589 |
+
shell.configurables.append(self)
|
| 590 |
+
if hasattr(shell, "config"):
|
| 591 |
+
kwargs.setdefault("parent", shell)
|
| 592 |
+
|
| 593 |
+
self.shell = shell
|
| 594 |
+
self.options_table = {}
|
| 595 |
+
# The method decorators are run when the instance doesn't exist yet, so
|
| 596 |
+
# they can only record the names of the methods they are supposed to
|
| 597 |
+
# grab. Only now, that the instance exists, can we create the proper
|
| 598 |
+
# mapping to bound methods. So we read the info off the original names
|
| 599 |
+
# table and replace each method name by the actual bound method.
|
| 600 |
+
# But we mustn't clobber the *class* mapping, in case of multiple instances.
|
| 601 |
+
class_magics = self.magics
|
| 602 |
+
self.magics = {}
|
| 603 |
+
for mtype in magic_kinds:
|
| 604 |
+
tab = self.magics[mtype] = {}
|
| 605 |
+
cls_tab = class_magics[mtype]
|
| 606 |
+
for magic_name, meth_name in cls_tab.items():
|
| 607 |
+
if isinstance(meth_name, str):
|
| 608 |
+
# it's a method name, grab it
|
| 609 |
+
tab[magic_name] = getattr(self, meth_name)
|
| 610 |
+
else:
|
| 611 |
+
# it's the real thing
|
| 612 |
+
tab[magic_name] = meth_name
|
| 613 |
+
# Configurable **needs** to be initiated at the end or the config
|
| 614 |
+
# magics get screwed up.
|
| 615 |
+
super(Magics, self).__init__(**kwargs)
|
| 616 |
+
|
| 617 |
+
def arg_err(self, func):
|
| 618 |
+
"""Print docstring if incorrect arguments were passed"""
|
| 619 |
+
print("Error in arguments:")
|
| 620 |
+
print(oinspect.getdoc(func))
|
| 621 |
+
|
| 622 |
+
def format_latex(self, strng):
|
| 623 |
+
"""Format a string for latex inclusion."""
|
| 624 |
+
|
| 625 |
+
# Characters that need to be escaped for latex:
|
| 626 |
+
escape_re = re.compile(r"(%|_|\$|#|&)", re.MULTILINE)
|
| 627 |
+
# Magic command names as headers:
|
| 628 |
+
cmd_name_re = re.compile(r"^(%s.*?):" % ESC_MAGIC, re.MULTILINE)
|
| 629 |
+
# Magic commands
|
| 630 |
+
cmd_re = re.compile(r"(?P<cmd>%s.+?\b)(?!\}\}:)" % ESC_MAGIC, re.MULTILINE)
|
| 631 |
+
# Paragraph continue
|
| 632 |
+
par_re = re.compile(r"\\$", re.MULTILINE)
|
| 633 |
+
|
| 634 |
+
# The "\n" symbol
|
| 635 |
+
newline_re = re.compile(r"\\n")
|
| 636 |
+
|
| 637 |
+
# Now build the string for output:
|
| 638 |
+
# strng = cmd_name_re.sub(r'\n\\texttt{\\textsl{\\large \1}}:',strng)
|
| 639 |
+
strng = cmd_name_re.sub(r"\n\\bigskip\n\\texttt{\\textbf{ \1}}:", strng)
|
| 640 |
+
strng = cmd_re.sub(r"\\texttt{\g<cmd>}", strng)
|
| 641 |
+
strng = par_re.sub(r"\\\\", strng)
|
| 642 |
+
strng = escape_re.sub(r"\\\1", strng)
|
| 643 |
+
strng = newline_re.sub(r"\\textbackslash{}n", strng)
|
| 644 |
+
return strng
|
| 645 |
+
|
| 646 |
+
def parse_options(self, arg_str, opt_str, *long_opts, **kw):
|
| 647 |
+
"""Parse options passed to an argument string.
|
| 648 |
+
|
| 649 |
+
The interface is similar to that of :func:`getopt.getopt`, but it
|
| 650 |
+
returns a :class:`~IPython.utils.struct.Struct` with the options as keys
|
| 651 |
+
and the stripped argument string still as a string.
|
| 652 |
+
|
| 653 |
+
arg_str is quoted as a true sys.argv vector by using shlex.split.
|
| 654 |
+
This allows us to easily expand variables, glob files, quote
|
| 655 |
+
arguments, etc.
|
| 656 |
+
|
| 657 |
+
Parameters
|
| 658 |
+
----------
|
| 659 |
+
arg_str : str
|
| 660 |
+
The arguments to parse.
|
| 661 |
+
opt_str : str
|
| 662 |
+
The options specification.
|
| 663 |
+
mode : str, default 'string'
|
| 664 |
+
If given as 'list', the argument string is returned as a list (split
|
| 665 |
+
on whitespace) instead of a string.
|
| 666 |
+
list_all : bool, default False
|
| 667 |
+
Put all option values in lists. Normally only options
|
| 668 |
+
appearing more than once are put in a list.
|
| 669 |
+
posix : bool, default True
|
| 670 |
+
Whether to split the input line in POSIX mode or not, as per the
|
| 671 |
+
conventions outlined in the :mod:`shlex` module from the standard
|
| 672 |
+
library.
|
| 673 |
+
"""
|
| 674 |
+
|
| 675 |
+
# inject default options at the beginning of the input line
|
| 676 |
+
caller = sys._getframe(1).f_code.co_name
|
| 677 |
+
arg_str = "%s %s" % (self.options_table.get(caller, ""), arg_str)
|
| 678 |
+
|
| 679 |
+
mode = kw.get("mode", "string")
|
| 680 |
+
if mode not in ["string", "list"]:
|
| 681 |
+
raise ValueError("incorrect mode given: %s" % mode)
|
| 682 |
+
# Get options
|
| 683 |
+
list_all = kw.get("list_all", 0)
|
| 684 |
+
posix = kw.get("posix", os.name == "posix")
|
| 685 |
+
strict = kw.get("strict", True)
|
| 686 |
+
|
| 687 |
+
preserve_non_opts = kw.get("preserve_non_opts", False)
|
| 688 |
+
remainder_arg_str = arg_str
|
| 689 |
+
|
| 690 |
+
# Check if we have more than one argument to warrant extra processing:
|
| 691 |
+
odict: dict[str, t.Any] = {} # Dictionary with options
|
| 692 |
+
args = arg_str.split()
|
| 693 |
+
if len(args) >= 1:
|
| 694 |
+
# If the list of inputs only has 0 or 1 thing in it, there's no
|
| 695 |
+
# need to look for options
|
| 696 |
+
argv = arg_split(arg_str, posix, strict)
|
| 697 |
+
# Do regular option processing
|
| 698 |
+
try:
|
| 699 |
+
opts, args = getopt(argv, opt_str, long_opts)
|
| 700 |
+
except GetoptError as e:
|
| 701 |
+
raise UsageError(
|
| 702 |
+
'%s (allowed: "%s"%s)'
|
| 703 |
+
% (e.msg, opt_str, " ".join(("",) + long_opts) if long_opts else "")
|
| 704 |
+
) from e
|
| 705 |
+
for o, a in opts:
|
| 706 |
+
if mode == "string" and preserve_non_opts:
|
| 707 |
+
# remove option-parts from the original args-string and preserve remaining-part.
|
| 708 |
+
# This relies on the arg_split(...) and getopt(...)'s impl spec, that the parsed options are
|
| 709 |
+
# returned in the original order.
|
| 710 |
+
remainder_arg_str = remainder_arg_str.replace(o, "", 1).replace(
|
| 711 |
+
a, "", 1
|
| 712 |
+
)
|
| 713 |
+
if o.startswith("--"):
|
| 714 |
+
o = o[2:]
|
| 715 |
+
else:
|
| 716 |
+
o = o[1:]
|
| 717 |
+
try:
|
| 718 |
+
odict[o].append(a)
|
| 719 |
+
except AttributeError:
|
| 720 |
+
odict[o] = [odict[o], a]
|
| 721 |
+
except KeyError:
|
| 722 |
+
if list_all:
|
| 723 |
+
odict[o] = [a]
|
| 724 |
+
else:
|
| 725 |
+
odict[o] = a
|
| 726 |
+
|
| 727 |
+
# Prepare opts,args for return
|
| 728 |
+
opts = Struct(odict) # type: ignore[assignment]
|
| 729 |
+
if mode == "string":
|
| 730 |
+
if preserve_non_opts:
|
| 731 |
+
args = remainder_arg_str.lstrip()
|
| 732 |
+
else:
|
| 733 |
+
args = " ".join(args)
|
| 734 |
+
|
| 735 |
+
return opts, args
|
| 736 |
+
|
| 737 |
+
def default_option(self, fn, optstr):
|
| 738 |
+
"""Make an entry in the options_table for fn, with value optstr"""
|
| 739 |
+
assert False, "is this even called?"
|
| 740 |
+
if fn not in self.lsmagic():
|
| 741 |
+
error("%s is not a magic function" % fn)
|
| 742 |
+
self.options_table[fn] = optstr
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
class MagicAlias:
|
| 746 |
+
"""An alias to another magic function.
|
| 747 |
+
|
| 748 |
+
An alias is determined by its magic name and magic kind. Lookup
|
| 749 |
+
is done at call time, so if the underlying magic changes the alias
|
| 750 |
+
will call the new function.
|
| 751 |
+
|
| 752 |
+
Use the :meth:`MagicsManager.register_alias` method or the
|
| 753 |
+
`%alias_magic` magic function to create and register a new alias.
|
| 754 |
+
"""
|
| 755 |
+
|
| 756 |
+
def __init__(self, shell, magic_name, magic_kind, magic_params=None):
|
| 757 |
+
self.shell = shell
|
| 758 |
+
self.magic_name = magic_name
|
| 759 |
+
self.magic_params = magic_params
|
| 760 |
+
self.magic_kind = magic_kind
|
| 761 |
+
|
| 762 |
+
self.pretty_target = "%s%s" % (magic_escapes[self.magic_kind], self.magic_name)
|
| 763 |
+
self.__doc__ = "Alias for `%s`." % self.pretty_target
|
| 764 |
+
|
| 765 |
+
self._in_call = False
|
| 766 |
+
|
| 767 |
+
def __call__(self, *args, **kwargs):
|
| 768 |
+
"""Call the magic alias."""
|
| 769 |
+
fn = self.shell.find_magic(self.magic_name, self.magic_kind)
|
| 770 |
+
if fn is None:
|
| 771 |
+
raise UsageError("Magic `%s` not found." % self.pretty_target)
|
| 772 |
+
|
| 773 |
+
# Protect against infinite recursion.
|
| 774 |
+
if self._in_call:
|
| 775 |
+
raise UsageError(
|
| 776 |
+
"Infinite recursion detected; magic aliases cannot call themselves."
|
| 777 |
+
)
|
| 778 |
+
self._in_call = True
|
| 779 |
+
try:
|
| 780 |
+
if self.magic_params:
|
| 781 |
+
args_list = list(args)
|
| 782 |
+
args_list[0] = self.magic_params + " " + args[0]
|
| 783 |
+
args = tuple(args_list)
|
| 784 |
+
return fn(*args, **kwargs)
|
| 785 |
+
finally:
|
| 786 |
+
self._in_call = False
|
lib/python3.12/site-packages/IPython/core/release.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Release data for the IPython project."""
|
| 3 |
+
|
| 4 |
+
#-----------------------------------------------------------------------------
|
| 5 |
+
# Copyright (c) 2008, IPython Development Team.
|
| 6 |
+
# Copyright (c) 2001, Fernando Perez <fernando.perez@colorado.edu>
|
| 7 |
+
# Copyright (c) 2001, Janko Hauser <jhauser@zscout.de>
|
| 8 |
+
# Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu>
|
| 9 |
+
#
|
| 10 |
+
# Distributed under the terms of the Modified BSD License.
|
| 11 |
+
#
|
| 12 |
+
# The full license is in the file COPYING.txt, distributed with this software.
|
| 13 |
+
#-----------------------------------------------------------------------------
|
| 14 |
+
|
| 15 |
+
# IPython version information. An empty _version_extra corresponds to a full
|
| 16 |
+
# release. 'dev' as a _version_extra string means this is a development
|
| 17 |
+
# version
|
| 18 |
+
_version_major = 9
|
| 19 |
+
_version_minor = 10
|
| 20 |
+
_version_patch = 0
|
| 21 |
+
_version_extra = ".dev"
|
| 22 |
+
# _version_extra = "b2"
|
| 23 |
+
_version_extra = "" # Uncomment this for full releases
|
| 24 |
+
|
| 25 |
+
# Construct full version string from these.
|
| 26 |
+
_ver = [_version_major, _version_minor, _version_patch]
|
| 27 |
+
|
| 28 |
+
__version__ = '.'.join(map(str, _ver))
|
| 29 |
+
if _version_extra:
|
| 30 |
+
__version__ = __version__ + _version_extra
|
| 31 |
+
|
| 32 |
+
version = __version__ # backwards compatibility name
|
| 33 |
+
version_info = (_version_major, _version_minor, _version_patch, _version_extra)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
license = "BSD-3-Clause"
|
| 37 |
+
|
| 38 |
+
authors = {
|
| 39 |
+
"Fernando": ("Fernando Perez", "fperez.net@gmail.com"),
|
| 40 |
+
"M": ("M Bussonnier", "mbussonnier@gmail.com"),
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
author = 'The IPython Development Team'
|
| 44 |
+
|
| 45 |
+
author_email = 'ipython-dev@python.org'
|
lib/python3.12/site-packages/IPython/display.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public API for display tools in IPython."""
|
| 2 |
+
|
| 3 |
+
# -----------------------------------------------------------------------------
|
| 4 |
+
# Copyright (C) 2012 The IPython Development Team
|
| 5 |
+
#
|
| 6 |
+
# Distributed under the terms of the BSD License. The full license is in
|
| 7 |
+
# the file COPYING, distributed as part of this software.
|
| 8 |
+
# -----------------------------------------------------------------------------
|
| 9 |
+
|
| 10 |
+
# -----------------------------------------------------------------------------
|
| 11 |
+
# Imports
|
| 12 |
+
# -----------------------------------------------------------------------------
|
| 13 |
+
|
| 14 |
+
from IPython.core.display_functions import *
|
| 15 |
+
from IPython.core.display import (
|
| 16 |
+
display_pretty as display_pretty,
|
| 17 |
+
display_html as display_html,
|
| 18 |
+
display_markdown as display_markdown,
|
| 19 |
+
display_svg as display_svg,
|
| 20 |
+
display_png as display_png,
|
| 21 |
+
display_jpeg as display_jpeg,
|
| 22 |
+
display_latex as display_latex,
|
| 23 |
+
display_json as display_json,
|
| 24 |
+
display_javascript as display_javascript,
|
| 25 |
+
display_pdf as display_pdf,
|
| 26 |
+
DisplayObject as DisplayObject,
|
| 27 |
+
TextDisplayObject as TextDisplayObject,
|
| 28 |
+
Pretty as Pretty,
|
| 29 |
+
HTML as HTML,
|
| 30 |
+
Markdown as Markdown,
|
| 31 |
+
Math as Math,
|
| 32 |
+
Latex as Latex,
|
| 33 |
+
SVG as SVG,
|
| 34 |
+
ProgressBar as ProgressBar,
|
| 35 |
+
JSON as JSON,
|
| 36 |
+
GeoJSON as GeoJSON,
|
| 37 |
+
Javascript as Javascript,
|
| 38 |
+
Image as Image,
|
| 39 |
+
Video as Video,
|
| 40 |
+
)
|
| 41 |
+
from IPython.lib.display import *
|
lib/python3.12/site-packages/IPython/paths.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Find files and directories which IPython uses.
|
| 2 |
+
"""
|
| 3 |
+
import os.path
|
| 4 |
+
import tempfile
|
| 5 |
+
from warnings import warn
|
| 6 |
+
|
| 7 |
+
import IPython
|
| 8 |
+
from IPython.utils.importstring import import_item
|
| 9 |
+
from IPython.utils.path import (
|
| 10 |
+
get_home_dir,
|
| 11 |
+
get_xdg_dir,
|
| 12 |
+
get_xdg_cache_dir,
|
| 13 |
+
compress_user,
|
| 14 |
+
_writable_dir,
|
| 15 |
+
ensure_dir_exists,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def get_ipython_dir() -> str:
|
| 20 |
+
"""Get the IPython directory for this platform and user.
|
| 21 |
+
|
| 22 |
+
This uses the logic in `get_home_dir` to find the home directory
|
| 23 |
+
and then adds .ipython to the end of the path.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
env = os.environ
|
| 27 |
+
pjoin = os.path.join
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
ipdir_def = '.ipython'
|
| 31 |
+
|
| 32 |
+
home_dir = get_home_dir()
|
| 33 |
+
xdg_dir = get_xdg_dir()
|
| 34 |
+
|
| 35 |
+
ipdir = env.get("IPYTHONDIR", None)
|
| 36 |
+
if ipdir is None:
|
| 37 |
+
# not set explicitly, use ~/.ipython
|
| 38 |
+
ipdir = pjoin(home_dir, ipdir_def)
|
| 39 |
+
if xdg_dir:
|
| 40 |
+
# Several IPython versions (up to 1.x) defaulted to .config/ipython
|
| 41 |
+
# on Linux. We have decided to go back to using .ipython everywhere
|
| 42 |
+
xdg_ipdir = pjoin(xdg_dir, 'ipython')
|
| 43 |
+
|
| 44 |
+
if _writable_dir(xdg_ipdir):
|
| 45 |
+
cu = compress_user
|
| 46 |
+
if os.path.exists(ipdir):
|
| 47 |
+
warn(('Ignoring {0} in favour of {1}. Remove {0} to '
|
| 48 |
+
'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
|
| 49 |
+
elif os.path.islink(xdg_ipdir):
|
| 50 |
+
warn(('{0} is deprecated. Move link to {1} to '
|
| 51 |
+
'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
|
| 52 |
+
else:
|
| 53 |
+
ipdir = xdg_ipdir
|
| 54 |
+
|
| 55 |
+
ipdir = os.path.normpath(os.path.expanduser(ipdir))
|
| 56 |
+
|
| 57 |
+
if os.path.exists(ipdir) and not _writable_dir(ipdir):
|
| 58 |
+
# ipdir exists, but is not writable
|
| 59 |
+
warn("IPython dir '{0}' is not a writable location,"
|
| 60 |
+
" using a temp directory.".format(ipdir))
|
| 61 |
+
ipdir = tempfile.mkdtemp()
|
| 62 |
+
elif not os.path.exists(ipdir):
|
| 63 |
+
parent = os.path.dirname(ipdir)
|
| 64 |
+
if not _writable_dir(parent):
|
| 65 |
+
# ipdir does not exist and parent isn't writable
|
| 66 |
+
warn("IPython parent '{0}' is not a writable location,"
|
| 67 |
+
" using a temp directory.".format(parent))
|
| 68 |
+
ipdir = tempfile.mkdtemp()
|
| 69 |
+
else:
|
| 70 |
+
os.makedirs(ipdir, exist_ok=True)
|
| 71 |
+
assert isinstance(ipdir, str), "all path manipulation should be str(unicode), but are not."
|
| 72 |
+
return ipdir
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_ipython_cache_dir() -> str:
|
| 76 |
+
"""Get the cache directory it is created if it does not exist."""
|
| 77 |
+
xdgdir = get_xdg_cache_dir()
|
| 78 |
+
if xdgdir is None:
|
| 79 |
+
return get_ipython_dir()
|
| 80 |
+
ipdir = os.path.join(xdgdir, "ipython")
|
| 81 |
+
if not os.path.exists(ipdir) and _writable_dir(xdgdir):
|
| 82 |
+
ensure_dir_exists(ipdir)
|
| 83 |
+
elif not _writable_dir(xdgdir):
|
| 84 |
+
return get_ipython_dir()
|
| 85 |
+
|
| 86 |
+
return ipdir
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def get_ipython_package_dir() -> str:
|
| 90 |
+
"""Get the base directory where IPython itself is installed."""
|
| 91 |
+
ipdir = os.path.dirname(IPython.__file__)
|
| 92 |
+
assert isinstance(ipdir, str)
|
| 93 |
+
return ipdir
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def get_ipython_module_path(module_str):
|
| 97 |
+
"""Find the path to an IPython module in this version of IPython.
|
| 98 |
+
|
| 99 |
+
This will always find the version of the module that is in this importable
|
| 100 |
+
IPython package. This will always return the path to the ``.py``
|
| 101 |
+
version of the module.
|
| 102 |
+
"""
|
| 103 |
+
if module_str == 'IPython':
|
| 104 |
+
return os.path.join(get_ipython_package_dir(), '__init__.py')
|
| 105 |
+
mod = import_item(module_str)
|
| 106 |
+
the_path = mod.__file__.replace('.pyc', '.py')
|
| 107 |
+
the_path = the_path.replace('.pyo', '.py')
|
| 108 |
+
return the_path
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def locate_profile(profile='default'):
|
| 112 |
+
"""Find the path to the folder associated with a given profile.
|
| 113 |
+
|
| 114 |
+
I.e. find $IPYTHONDIR/profile_whatever.
|
| 115 |
+
"""
|
| 116 |
+
from IPython.core.profiledir import ProfileDir, ProfileDirError
|
| 117 |
+
try:
|
| 118 |
+
pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
|
| 119 |
+
except ProfileDirError as e:
|
| 120 |
+
# IOError makes more sense when people are expecting a path
|
| 121 |
+
raise IOError("Couldn't find profile %r" % profile) from e
|
| 122 |
+
return pd.location
|
lib/python3.12/site-packages/IPython/py.typed
ADDED
|
File without changes
|
lib/python3.12/site-packages/IPython/sphinxext/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/IPython/sphinxext/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (196 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/sphinxext/__pycache__/custom_doctests.cpython-312.pyc
ADDED
|
Binary file (4.86 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/sphinxext/__pycache__/ipython_console_highlighting.cpython-312.pyc
ADDED
|
Binary file (773 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/sphinxext/__pycache__/ipython_directive.cpython-312.pyc
ADDED
|
Binary file (40.3 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/sphinxext/custom_doctests.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Handlers for IPythonDirective's @doctest pseudo-decorator.
|
| 3 |
+
|
| 4 |
+
The Sphinx extension that provides support for embedded IPython code provides
|
| 5 |
+
a pseudo-decorator @doctest, which treats the input/output block as a
|
| 6 |
+
doctest, raising a RuntimeError during doc generation if the actual output
|
| 7 |
+
(after running the input) does not match the expected output.
|
| 8 |
+
|
| 9 |
+
An example usage is:
|
| 10 |
+
|
| 11 |
+
.. code-block:: rst
|
| 12 |
+
|
| 13 |
+
.. ipython::
|
| 14 |
+
|
| 15 |
+
In [1]: x = 1
|
| 16 |
+
|
| 17 |
+
@doctest
|
| 18 |
+
In [2]: x + 2
|
| 19 |
+
Out[3]: 3
|
| 20 |
+
|
| 21 |
+
One can also provide arguments to the decorator. The first argument should be
|
| 22 |
+
the name of a custom handler. The specification of any other arguments is
|
| 23 |
+
determined by the handler. For example,
|
| 24 |
+
|
| 25 |
+
.. code-block:: rst
|
| 26 |
+
|
| 27 |
+
.. ipython::
|
| 28 |
+
|
| 29 |
+
@doctest float
|
| 30 |
+
In [154]: 0.1 + 0.2
|
| 31 |
+
Out[154]: 0.3
|
| 32 |
+
|
| 33 |
+
allows the actual output ``0.30000000000000004`` to match the expected output
|
| 34 |
+
due to a comparison with `np.allclose`.
|
| 35 |
+
|
| 36 |
+
This module contains handlers for the @doctest pseudo-decorator. Handlers
|
| 37 |
+
should have the following function signature::
|
| 38 |
+
|
| 39 |
+
handler(sphinx_shell, args, input_lines, found, submitted)
|
| 40 |
+
|
| 41 |
+
where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list
|
| 42 |
+
of arguments that follow: '@doctest handler_name', `input_lines` contains
|
| 43 |
+
a list of the lines relevant to the current doctest, `found` is a string
|
| 44 |
+
containing the output from the IPython shell, and `submitted` is a string
|
| 45 |
+
containing the expected output from the IPython shell.
|
| 46 |
+
|
| 47 |
+
Handlers must be registered in the `doctests` dict at the end of this module.
|
| 48 |
+
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
def str_to_array(s):
|
| 52 |
+
"""
|
| 53 |
+
Simplistic converter of strings from repr to float NumPy arrays.
|
| 54 |
+
|
| 55 |
+
If the repr representation has ellipsis in it, then this will fail.
|
| 56 |
+
|
| 57 |
+
Parameters
|
| 58 |
+
----------
|
| 59 |
+
s : str
|
| 60 |
+
The repr version of a NumPy array.
|
| 61 |
+
|
| 62 |
+
Examples
|
| 63 |
+
--------
|
| 64 |
+
>>> s = "array([ 0.3, inf, nan])"
|
| 65 |
+
>>> a = str_to_array(s)
|
| 66 |
+
|
| 67 |
+
"""
|
| 68 |
+
import numpy as np
|
| 69 |
+
|
| 70 |
+
# Need to make sure eval() knows about inf and nan.
|
| 71 |
+
# This also assumes default printoptions for NumPy.
|
| 72 |
+
from numpy import inf, nan
|
| 73 |
+
|
| 74 |
+
if s.startswith(u'array'):
|
| 75 |
+
# Remove array( and )
|
| 76 |
+
s = s[6:-1]
|
| 77 |
+
|
| 78 |
+
if s.startswith(u'['):
|
| 79 |
+
a = np.array(eval(s), dtype=float)
|
| 80 |
+
else:
|
| 81 |
+
# Assume its a regular float. Force 1D so we can index into it.
|
| 82 |
+
a = np.atleast_1d(float(s))
|
| 83 |
+
return a
|
| 84 |
+
|
| 85 |
+
def float_doctest(sphinx_shell, args, input_lines, found, submitted):
|
| 86 |
+
"""
|
| 87 |
+
Doctest which allow the submitted output to vary slightly from the input.
|
| 88 |
+
|
| 89 |
+
Here is how it might appear in an rst file:
|
| 90 |
+
|
| 91 |
+
.. code-block:: rst
|
| 92 |
+
|
| 93 |
+
.. ipython::
|
| 94 |
+
|
| 95 |
+
@doctest float
|
| 96 |
+
In [1]: 0.1 + 0.2
|
| 97 |
+
Out[1]: 0.3
|
| 98 |
+
|
| 99 |
+
"""
|
| 100 |
+
import numpy as np
|
| 101 |
+
|
| 102 |
+
if len(args) == 2:
|
| 103 |
+
rtol = 1e-05
|
| 104 |
+
atol = 1e-08
|
| 105 |
+
else:
|
| 106 |
+
# Both must be specified if any are specified.
|
| 107 |
+
try:
|
| 108 |
+
rtol = float(args[2])
|
| 109 |
+
atol = float(args[3])
|
| 110 |
+
except IndexError:
|
| 111 |
+
e = ("Both `rtol` and `atol` must be specified "
|
| 112 |
+
"if either are specified: {0}".format(args))
|
| 113 |
+
raise IndexError(e) from e
|
| 114 |
+
|
| 115 |
+
try:
|
| 116 |
+
submitted = str_to_array(submitted)
|
| 117 |
+
found = str_to_array(found)
|
| 118 |
+
except:
|
| 119 |
+
# For example, if the array is huge and there are ellipsis in it.
|
| 120 |
+
error = True
|
| 121 |
+
else:
|
| 122 |
+
found_isnan = np.isnan(found)
|
| 123 |
+
submitted_isnan = np.isnan(submitted)
|
| 124 |
+
error = not np.allclose(found_isnan, submitted_isnan)
|
| 125 |
+
error |= not np.allclose(found[~found_isnan],
|
| 126 |
+
submitted[~submitted_isnan],
|
| 127 |
+
rtol=rtol, atol=atol)
|
| 128 |
+
|
| 129 |
+
TAB = ' ' * 4
|
| 130 |
+
directive = sphinx_shell.directive
|
| 131 |
+
if directive is None:
|
| 132 |
+
source = 'Unavailable'
|
| 133 |
+
content = 'Unavailable'
|
| 134 |
+
else:
|
| 135 |
+
source = directive.state.document.current_source
|
| 136 |
+
# Add tabs and make into a single string.
|
| 137 |
+
content = '\n'.join([TAB + line for line in directive.content])
|
| 138 |
+
|
| 139 |
+
if error:
|
| 140 |
+
|
| 141 |
+
e = ('doctest float comparison failure\n\n'
|
| 142 |
+
'Document source: {0}\n\n'
|
| 143 |
+
'Raw content: \n{1}\n\n'
|
| 144 |
+
'On input line(s):\n{TAB}{2}\n\n'
|
| 145 |
+
'we found output:\n{TAB}{3}\n\n'
|
| 146 |
+
'instead of the expected:\n{TAB}{4}\n\n')
|
| 147 |
+
e = e.format(source, content, '\n'.join(input_lines), repr(found),
|
| 148 |
+
repr(submitted), TAB=TAB)
|
| 149 |
+
raise RuntimeError(e)
|
| 150 |
+
|
| 151 |
+
# dict of allowable doctest handlers. The key represents the first argument
|
| 152 |
+
# that must be given to @doctest in order to activate the handler.
|
| 153 |
+
doctests = {
|
| 154 |
+
'float': float_doctest,
|
| 155 |
+
}
|
lib/python3.12/site-packages/IPython/sphinxext/ipython_console_highlighting.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
reST directive for syntax-highlighting ipython interactive sessions.
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from sphinx import highlighting
|
| 7 |
+
from ipython_pygments_lexers import IPyLexer
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def setup(app):
|
| 11 |
+
"""Setup as a sphinx extension."""
|
| 12 |
+
|
| 13 |
+
# This is only a lexer, so adding it below to pygments appears sufficient.
|
| 14 |
+
# But if somebody knows what the right API usage should be to do that via
|
| 15 |
+
# sphinx, by all means fix it here. At least having this setup.py
|
| 16 |
+
# suppresses the sphinx warning we'd get without it.
|
| 17 |
+
metadata = {"parallel_read_safe": True, "parallel_write_safe": True}
|
| 18 |
+
return metadata
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
# Register the extension as a valid pygments lexer.
|
| 22 |
+
# Alternatively, we could register the lexer with pygments instead. This would
|
| 23 |
+
# require using setuptools entrypoints: http://pygments.org/docs/plugins
|
| 24 |
+
|
| 25 |
+
ipy3 = IPyLexer()
|
| 26 |
+
|
| 27 |
+
highlighting.lexers["ipython"] = ipy3
|
| 28 |
+
highlighting.lexers["ipython3"] = ipy3
|
lib/python3.12/site-packages/IPython/sphinxext/ipython_directive.py
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Sphinx directive to support embedded IPython code.
|
| 4 |
+
|
| 5 |
+
IPython provides an extension for `Sphinx <http://www.sphinx-doc.org/>`_ to
|
| 6 |
+
highlight and run code.
|
| 7 |
+
|
| 8 |
+
This directive allows pasting of entire interactive IPython sessions, prompts
|
| 9 |
+
and all, and their code will actually get re-executed at doc build time, with
|
| 10 |
+
all prompts renumbered sequentially. It also allows you to input code as a pure
|
| 11 |
+
python input by giving the argument python to the directive. The output looks
|
| 12 |
+
like an interactive ipython section.
|
| 13 |
+
|
| 14 |
+
Here is an example of how the IPython directive can
|
| 15 |
+
**run** python code, at build time.
|
| 16 |
+
|
| 17 |
+
.. ipython::
|
| 18 |
+
|
| 19 |
+
In [1]: 1+1
|
| 20 |
+
|
| 21 |
+
In [1]: import datetime
|
| 22 |
+
...: datetime.date.fromisoformat('2022-02-22')
|
| 23 |
+
|
| 24 |
+
It supports IPython construct that plain
|
| 25 |
+
Python does not understand (like magics):
|
| 26 |
+
|
| 27 |
+
.. ipython::
|
| 28 |
+
|
| 29 |
+
In [0]: import time
|
| 30 |
+
|
| 31 |
+
In [0]: %pdoc time.sleep
|
| 32 |
+
|
| 33 |
+
This will also support top-level async when using IPython 7.0+
|
| 34 |
+
|
| 35 |
+
.. ipython::
|
| 36 |
+
|
| 37 |
+
In [2]: import asyncio
|
| 38 |
+
...: print('before')
|
| 39 |
+
...: await asyncio.sleep(1)
|
| 40 |
+
...: print('after')
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
The namespace will persist across multiple code chucks, Let's define a variable:
|
| 44 |
+
|
| 45 |
+
.. ipython::
|
| 46 |
+
|
| 47 |
+
In [0]: who = "World"
|
| 48 |
+
|
| 49 |
+
And now say hello:
|
| 50 |
+
|
| 51 |
+
.. ipython::
|
| 52 |
+
|
| 53 |
+
In [0]: print('Hello,', who)
|
| 54 |
+
|
| 55 |
+
If the current section raises an exception, you can add the ``:okexcept:`` flag
|
| 56 |
+
to the current block, otherwise the build will fail.
|
| 57 |
+
|
| 58 |
+
.. ipython::
|
| 59 |
+
:okexcept:
|
| 60 |
+
|
| 61 |
+
In [1]: 1/0
|
| 62 |
+
|
| 63 |
+
IPython Sphinx directive module
|
| 64 |
+
===============================
|
| 65 |
+
|
| 66 |
+
To enable this directive, simply list it in your Sphinx ``conf.py`` file
|
| 67 |
+
(making sure the directory where you placed it is visible to sphinx, as is
|
| 68 |
+
needed for all Sphinx directives). For example, to enable syntax highlighting
|
| 69 |
+
and the IPython directive::
|
| 70 |
+
|
| 71 |
+
extensions = ['IPython.sphinxext.ipython_console_highlighting',
|
| 72 |
+
'IPython.sphinxext.ipython_directive']
|
| 73 |
+
|
| 74 |
+
The IPython directive outputs code-blocks with the language 'ipython'. So
|
| 75 |
+
if you do not have the syntax highlighting extension enabled as well, then
|
| 76 |
+
all rendered code-blocks will be uncolored. By default this directive assumes
|
| 77 |
+
that your prompts are unchanged IPython ones, but this can be customized.
|
| 78 |
+
The configurable options that can be placed in conf.py are:
|
| 79 |
+
|
| 80 |
+
ipython_savefig_dir:
|
| 81 |
+
The directory in which to save the figures. This is relative to the
|
| 82 |
+
Sphinx source directory. The default is `html_static_path`.
|
| 83 |
+
ipython_rgxin:
|
| 84 |
+
The compiled regular expression to denote the start of IPython input
|
| 85 |
+
lines. The default is ``re.compile('In \\[(\\d+)\\]:\\s?(.*)\\s*')``. You
|
| 86 |
+
shouldn't need to change this.
|
| 87 |
+
ipython_warning_is_error: [default to True]
|
| 88 |
+
Fail the build if something unexpected happen, for example if a block raise
|
| 89 |
+
an exception but does not have the `:okexcept:` flag. The exact behavior of
|
| 90 |
+
what is considered strict, may change between the sphinx directive version.
|
| 91 |
+
ipython_rgxout:
|
| 92 |
+
The compiled regular expression to denote the start of IPython output
|
| 93 |
+
lines. The default is ``re.compile('Out\\[(\\d+)\\]:\\s?(.*)\\s*')``. You
|
| 94 |
+
shouldn't need to change this.
|
| 95 |
+
ipython_promptin:
|
| 96 |
+
The string to represent the IPython input prompt in the generated ReST.
|
| 97 |
+
The default is ``'In [%d]:'``. This expects that the line numbers are used
|
| 98 |
+
in the prompt.
|
| 99 |
+
ipython_promptout:
|
| 100 |
+
The string to represent the IPython prompt in the generated ReST. The
|
| 101 |
+
default is ``'Out [%d]:'``. This expects that the line numbers are used
|
| 102 |
+
in the prompt.
|
| 103 |
+
ipython_mplbackend:
|
| 104 |
+
The string which specifies if the embedded Sphinx shell should import
|
| 105 |
+
Matplotlib and set the backend. The value specifies a backend that is
|
| 106 |
+
passed to `matplotlib.use()` before any lines in `ipython_execlines` are
|
| 107 |
+
executed. If not specified in conf.py, then the default value of 'agg' is
|
| 108 |
+
used. To use the IPython directive without matplotlib as a dependency, set
|
| 109 |
+
the value to `None`. It may end up that matplotlib is still imported
|
| 110 |
+
if the user specifies so in `ipython_execlines` or makes use of the
|
| 111 |
+
@savefig pseudo decorator.
|
| 112 |
+
ipython_execlines:
|
| 113 |
+
A list of strings to be exec'd in the embedded Sphinx shell. Typical
|
| 114 |
+
usage is to make certain packages always available. Set this to an empty
|
| 115 |
+
list if you wish to have no imports always available. If specified in
|
| 116 |
+
``conf.py`` as `None`, then it has the effect of making no imports available.
|
| 117 |
+
If omitted from conf.py altogether, then the default value of
|
| 118 |
+
['import numpy as np', 'import matplotlib.pyplot as plt'] is used.
|
| 119 |
+
ipython_holdcount
|
| 120 |
+
When the @suppress pseudo-decorator is used, the execution count can be
|
| 121 |
+
incremented or not. The default behavior is to hold the execution count,
|
| 122 |
+
corresponding to a value of `True`. Set this to `False` to increment
|
| 123 |
+
the execution count after each suppressed command.
|
| 124 |
+
|
| 125 |
+
As an example, to use the IPython directive when `matplotlib` is not available,
|
| 126 |
+
one sets the backend to `None`::
|
| 127 |
+
|
| 128 |
+
ipython_mplbackend = None
|
| 129 |
+
|
| 130 |
+
An example usage of the directive is:
|
| 131 |
+
|
| 132 |
+
.. code-block:: rst
|
| 133 |
+
|
| 134 |
+
.. ipython::
|
| 135 |
+
|
| 136 |
+
In [1]: x = 1
|
| 137 |
+
|
| 138 |
+
In [2]: y = x**2
|
| 139 |
+
|
| 140 |
+
In [3]: print(y)
|
| 141 |
+
|
| 142 |
+
See http://matplotlib.org/sampledoc/ipython_directive.html for additional
|
| 143 |
+
documentation.
|
| 144 |
+
|
| 145 |
+
Pseudo-Decorators
|
| 146 |
+
=================
|
| 147 |
+
|
| 148 |
+
Note: Only one decorator is supported per input. If more than one decorator
|
| 149 |
+
is specified, then only the last one is used.
|
| 150 |
+
|
| 151 |
+
In addition to the Pseudo-Decorators/options described at the above link,
|
| 152 |
+
several enhancements have been made. The directive will emit a message to the
|
| 153 |
+
console at build-time if code-execution resulted in an exception or warning.
|
| 154 |
+
You can suppress these on a per-block basis by specifying the :okexcept:
|
| 155 |
+
or :okwarning: options:
|
| 156 |
+
|
| 157 |
+
.. code-block:: rst
|
| 158 |
+
|
| 159 |
+
.. ipython::
|
| 160 |
+
:okexcept:
|
| 161 |
+
:okwarning:
|
| 162 |
+
|
| 163 |
+
In [1]: 1/0
|
| 164 |
+
In [2]: # raise warning.
|
| 165 |
+
|
| 166 |
+
To Do
|
| 167 |
+
=====
|
| 168 |
+
|
| 169 |
+
- Turn the ad-hoc test() function into a real test suite.
|
| 170 |
+
- Break up ipython-specific functionality from matplotlib stuff into better
|
| 171 |
+
separated code.
|
| 172 |
+
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
# Authors
|
| 176 |
+
# =======
|
| 177 |
+
#
|
| 178 |
+
# - John D Hunter: original author.
|
| 179 |
+
# - Fernando Perez: refactoring, documentation, cleanups, port to 0.11.
|
| 180 |
+
# - VáclavŠmilauer <eudoxos-AT-arcig.cz>: Prompt generalizations.
|
| 181 |
+
# - Skipper Seabold, refactoring, cleanups, pure python addition
|
| 182 |
+
|
| 183 |
+
#-----------------------------------------------------------------------------
|
| 184 |
+
# Imports
|
| 185 |
+
#-----------------------------------------------------------------------------
|
| 186 |
+
|
| 187 |
+
# Stdlib
|
| 188 |
+
import atexit
|
| 189 |
+
import errno
|
| 190 |
+
import os
|
| 191 |
+
import pathlib
|
| 192 |
+
import re
|
| 193 |
+
import sys
|
| 194 |
+
import tempfile
|
| 195 |
+
import ast
|
| 196 |
+
import warnings
|
| 197 |
+
import shutil
|
| 198 |
+
from io import StringIO
|
| 199 |
+
from typing import Any, Dict, Set
|
| 200 |
+
|
| 201 |
+
# Third-party
|
| 202 |
+
from docutils.parsers.rst import directives
|
| 203 |
+
from docutils.parsers.rst import Directive
|
| 204 |
+
from sphinx.util import logging
|
| 205 |
+
|
| 206 |
+
# Our own
|
| 207 |
+
from traitlets.config import Config
|
| 208 |
+
from IPython import InteractiveShell
|
| 209 |
+
from IPython.core.profiledir import ProfileDir
|
| 210 |
+
|
| 211 |
+
use_matplotlib = False
|
| 212 |
+
try:
|
| 213 |
+
import matplotlib
|
| 214 |
+
use_matplotlib = True
|
| 215 |
+
except Exception:
|
| 216 |
+
pass
|
| 217 |
+
|
| 218 |
+
#-----------------------------------------------------------------------------
|
| 219 |
+
# Globals
|
| 220 |
+
#-----------------------------------------------------------------------------
|
| 221 |
+
# for tokenizing blocks
|
| 222 |
+
COMMENT, INPUT, OUTPUT = range(3)
|
| 223 |
+
|
| 224 |
+
PSEUDO_DECORATORS = ["suppress", "verbatim", "savefig", "doctest"]
|
| 225 |
+
|
| 226 |
+
#-----------------------------------------------------------------------------
|
| 227 |
+
# Functions and class declarations
|
| 228 |
+
#-----------------------------------------------------------------------------
|
| 229 |
+
|
| 230 |
+
def block_parser(part, rgxin, rgxout, fmtin, fmtout):
|
| 231 |
+
"""
|
| 232 |
+
part is a string of ipython text, comprised of at most one
|
| 233 |
+
input, one output, comments, and blank lines. The block parser
|
| 234 |
+
parses the text into a list of::
|
| 235 |
+
|
| 236 |
+
blocks = [ (TOKEN0, data0), (TOKEN1, data1), ...]
|
| 237 |
+
|
| 238 |
+
where TOKEN is one of [COMMENT | INPUT | OUTPUT ] and
|
| 239 |
+
data is, depending on the type of token::
|
| 240 |
+
|
| 241 |
+
COMMENT : the comment string
|
| 242 |
+
|
| 243 |
+
INPUT: the (DECORATOR, INPUT_LINE, REST) where
|
| 244 |
+
DECORATOR: the input decorator (or None)
|
| 245 |
+
INPUT_LINE: the input as string (possibly multi-line)
|
| 246 |
+
REST : any stdout generated by the input line (not OUTPUT)
|
| 247 |
+
|
| 248 |
+
OUTPUT: the output string, possibly multi-line
|
| 249 |
+
|
| 250 |
+
"""
|
| 251 |
+
block = []
|
| 252 |
+
lines = part.split('\n')
|
| 253 |
+
N = len(lines)
|
| 254 |
+
i = 0
|
| 255 |
+
decorator = None
|
| 256 |
+
while 1:
|
| 257 |
+
|
| 258 |
+
if i==N:
|
| 259 |
+
# nothing left to parse -- the last line
|
| 260 |
+
break
|
| 261 |
+
|
| 262 |
+
line = lines[i]
|
| 263 |
+
i += 1
|
| 264 |
+
line_stripped = line.strip()
|
| 265 |
+
if line_stripped.startswith('#'):
|
| 266 |
+
block.append((COMMENT, line))
|
| 267 |
+
continue
|
| 268 |
+
|
| 269 |
+
if any(
|
| 270 |
+
line_stripped.startswith("@" + pseudo_decorator)
|
| 271 |
+
for pseudo_decorator in PSEUDO_DECORATORS
|
| 272 |
+
):
|
| 273 |
+
if decorator:
|
| 274 |
+
raise RuntimeError(
|
| 275 |
+
"Applying multiple pseudo-decorators on one line is not supported"
|
| 276 |
+
)
|
| 277 |
+
else:
|
| 278 |
+
decorator = line_stripped
|
| 279 |
+
continue
|
| 280 |
+
|
| 281 |
+
# does this look like an input line?
|
| 282 |
+
matchin = rgxin.match(line)
|
| 283 |
+
if matchin:
|
| 284 |
+
lineno, inputline = int(matchin.group(1)), matchin.group(2)
|
| 285 |
+
|
| 286 |
+
# the ....: continuation string
|
| 287 |
+
continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
|
| 288 |
+
Nc = len(continuation)
|
| 289 |
+
# input lines can continue on for more than one line, if
|
| 290 |
+
# we have a '\' line continuation char or a function call
|
| 291 |
+
# echo line 'print'. The input line can only be
|
| 292 |
+
# terminated by the end of the block or an output line, so
|
| 293 |
+
# we parse out the rest of the input line if it is
|
| 294 |
+
# multiline as well as any echo text
|
| 295 |
+
|
| 296 |
+
rest = []
|
| 297 |
+
while i<N:
|
| 298 |
+
|
| 299 |
+
# look ahead; if the next line is blank, or a comment, or
|
| 300 |
+
# an output line, we're done
|
| 301 |
+
|
| 302 |
+
nextline = lines[i]
|
| 303 |
+
matchout = rgxout.match(nextline)
|
| 304 |
+
# print("nextline=%s, continuation=%s, starts=%s"%(nextline, continuation, nextline.startswith(continuation)))
|
| 305 |
+
if matchout or nextline.startswith('#'):
|
| 306 |
+
break
|
| 307 |
+
elif nextline.startswith(continuation):
|
| 308 |
+
# The default ipython_rgx* treat the space following the colon as optional.
|
| 309 |
+
# However, If the space is there we must consume it or code
|
| 310 |
+
# employing the cython_magic extension will fail to execute.
|
| 311 |
+
#
|
| 312 |
+
# This works with the default ipython_rgx* patterns,
|
| 313 |
+
# If you modify them, YMMV.
|
| 314 |
+
nextline = nextline[Nc:]
|
| 315 |
+
if nextline and nextline[0] == ' ':
|
| 316 |
+
nextline = nextline[1:]
|
| 317 |
+
|
| 318 |
+
inputline += '\n' + nextline
|
| 319 |
+
else:
|
| 320 |
+
rest.append(nextline)
|
| 321 |
+
i+= 1
|
| 322 |
+
|
| 323 |
+
block.append((INPUT, (decorator, inputline, '\n'.join(rest))))
|
| 324 |
+
continue
|
| 325 |
+
|
| 326 |
+
# if it looks like an output line grab all the text to the end
|
| 327 |
+
# of the block
|
| 328 |
+
matchout = rgxout.match(line)
|
| 329 |
+
if matchout:
|
| 330 |
+
lineno, output = int(matchout.group(1)), matchout.group(2)
|
| 331 |
+
if i<N-1:
|
| 332 |
+
output = '\n'.join([output] + lines[i:])
|
| 333 |
+
|
| 334 |
+
block.append((OUTPUT, output))
|
| 335 |
+
break
|
| 336 |
+
|
| 337 |
+
return block
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class EmbeddedSphinxShell:
|
| 341 |
+
"""An embedded IPython instance to run inside Sphinx"""
|
| 342 |
+
|
| 343 |
+
def __init__(self, exec_lines=None):
|
| 344 |
+
|
| 345 |
+
self.cout = StringIO()
|
| 346 |
+
|
| 347 |
+
if exec_lines is None:
|
| 348 |
+
exec_lines = []
|
| 349 |
+
|
| 350 |
+
# Create config object for IPython
|
| 351 |
+
config = Config()
|
| 352 |
+
config.HistoryManager.hist_file = ':memory:'
|
| 353 |
+
config.InteractiveShell.autocall = False
|
| 354 |
+
config.InteractiveShell.autoindent = False
|
| 355 |
+
config.InteractiveShell.colors = "nocolor"
|
| 356 |
+
|
| 357 |
+
# create a profile so instance history isn't saved
|
| 358 |
+
tmp_profile_dir = tempfile.mkdtemp(prefix='profile_')
|
| 359 |
+
profname = 'auto_profile_sphinx_build'
|
| 360 |
+
pdir = os.path.join(tmp_profile_dir,profname)
|
| 361 |
+
profile = ProfileDir.create_profile_dir(pdir)
|
| 362 |
+
|
| 363 |
+
# Create and initialize global ipython, but don't start its mainloop.
|
| 364 |
+
# This will persist across different EmbeddedSphinxShell instances.
|
| 365 |
+
IP = InteractiveShell.instance(config=config, profile_dir=profile)
|
| 366 |
+
atexit.register(self.cleanup)
|
| 367 |
+
|
| 368 |
+
# Store a few parts of IPython we'll need.
|
| 369 |
+
self.IP = IP
|
| 370 |
+
self.user_ns = self.IP.user_ns
|
| 371 |
+
self.user_global_ns = self.IP.user_global_ns
|
| 372 |
+
|
| 373 |
+
self.input = ''
|
| 374 |
+
self.output = ''
|
| 375 |
+
self.tmp_profile_dir = tmp_profile_dir
|
| 376 |
+
|
| 377 |
+
self.is_verbatim = False
|
| 378 |
+
self.is_doctest = False
|
| 379 |
+
self.is_suppress = False
|
| 380 |
+
|
| 381 |
+
# Optionally, provide more detailed information to shell.
|
| 382 |
+
# this is assigned by the SetUp method of IPythonDirective
|
| 383 |
+
# to point at itself.
|
| 384 |
+
#
|
| 385 |
+
# So, you can access handy things at self.directive.state
|
| 386 |
+
self.directive = None
|
| 387 |
+
|
| 388 |
+
# on the first call to the savefig decorator, we'll import
|
| 389 |
+
# pyplot as plt so we can make a call to the plt.gcf().savefig
|
| 390 |
+
self._pyplot_imported = False
|
| 391 |
+
|
| 392 |
+
# Prepopulate the namespace.
|
| 393 |
+
for line in exec_lines:
|
| 394 |
+
self.process_input_line(line, store_history=False)
|
| 395 |
+
|
| 396 |
+
def cleanup(self):
|
| 397 |
+
shutil.rmtree(self.tmp_profile_dir, ignore_errors=True)
|
| 398 |
+
|
| 399 |
+
def clear_cout(self):
|
| 400 |
+
self.cout.seek(0)
|
| 401 |
+
self.cout.truncate(0)
|
| 402 |
+
|
| 403 |
+
def process_input_line(self, line, store_history):
|
| 404 |
+
return self.process_input_lines([line], store_history=store_history)
|
| 405 |
+
|
| 406 |
+
def process_input_lines(self, lines, store_history=True):
|
| 407 |
+
"""process the input, capturing stdout"""
|
| 408 |
+
stdout = sys.stdout
|
| 409 |
+
source_raw = '\n'.join(lines)
|
| 410 |
+
try:
|
| 411 |
+
sys.stdout = self.cout
|
| 412 |
+
self.IP.run_cell(source_raw, store_history=store_history)
|
| 413 |
+
finally:
|
| 414 |
+
sys.stdout = stdout
|
| 415 |
+
|
| 416 |
+
def process_image(self, decorator):
|
| 417 |
+
"""
|
| 418 |
+
# build out an image directive like
|
| 419 |
+
# .. image:: somefile.png
|
| 420 |
+
# :width 4in
|
| 421 |
+
#
|
| 422 |
+
# from an input like
|
| 423 |
+
# savefig somefile.png width=4in
|
| 424 |
+
"""
|
| 425 |
+
savefig_dir = self.savefig_dir
|
| 426 |
+
source_dir = self.source_dir
|
| 427 |
+
saveargs = decorator.split(' ')
|
| 428 |
+
filename = saveargs[1]
|
| 429 |
+
# insert relative path to image file in source
|
| 430 |
+
# as absolute path for Sphinx
|
| 431 |
+
# sphinx expects a posix path, even on Windows
|
| 432 |
+
path = pathlib.Path(savefig_dir, filename)
|
| 433 |
+
outfile = '/' + path.relative_to(source_dir).as_posix()
|
| 434 |
+
|
| 435 |
+
imagerows = ['.. image:: %s' % outfile]
|
| 436 |
+
|
| 437 |
+
for kwarg in saveargs[2:]:
|
| 438 |
+
arg, val = kwarg.split('=')
|
| 439 |
+
arg = arg.strip()
|
| 440 |
+
val = val.strip()
|
| 441 |
+
imagerows.append(' :%s: %s'%(arg, val))
|
| 442 |
+
|
| 443 |
+
image_file = os.path.basename(outfile) # only return file name
|
| 444 |
+
image_directive = '\n'.join(imagerows)
|
| 445 |
+
return image_file, image_directive
|
| 446 |
+
|
| 447 |
+
# Callbacks for each type of token
|
| 448 |
+
def process_input(self, data, input_prompt, lineno):
|
| 449 |
+
"""
|
| 450 |
+
Process data block for INPUT token.
|
| 451 |
+
|
| 452 |
+
"""
|
| 453 |
+
decorator, input, rest = data
|
| 454 |
+
image_file = None
|
| 455 |
+
image_directive = None
|
| 456 |
+
|
| 457 |
+
is_verbatim = decorator=='@verbatim' or self.is_verbatim
|
| 458 |
+
is_doctest = (decorator is not None and \
|
| 459 |
+
decorator.startswith('@doctest')) or self.is_doctest
|
| 460 |
+
is_suppress = decorator=='@suppress' or self.is_suppress
|
| 461 |
+
is_okexcept = decorator=='@okexcept' or self.is_okexcept
|
| 462 |
+
is_okwarning = decorator=='@okwarning' or self.is_okwarning
|
| 463 |
+
is_savefig = decorator is not None and \
|
| 464 |
+
decorator.startswith('@savefig')
|
| 465 |
+
|
| 466 |
+
input_lines = input.split('\n')
|
| 467 |
+
if len(input_lines) > 1:
|
| 468 |
+
if input_lines[-1] != "":
|
| 469 |
+
input_lines.append('') # make sure there's a blank line
|
| 470 |
+
# so splitter buffer gets reset
|
| 471 |
+
|
| 472 |
+
continuation = ' %s:'%''.join(['.']*(len(str(lineno))+2))
|
| 473 |
+
|
| 474 |
+
if is_savefig:
|
| 475 |
+
image_file, image_directive = self.process_image(decorator)
|
| 476 |
+
|
| 477 |
+
ret = []
|
| 478 |
+
is_semicolon = False
|
| 479 |
+
|
| 480 |
+
# Hold the execution count, if requested to do so.
|
| 481 |
+
if is_suppress and self.hold_count:
|
| 482 |
+
store_history = False
|
| 483 |
+
else:
|
| 484 |
+
store_history = True
|
| 485 |
+
|
| 486 |
+
# Note: catch_warnings is not thread safe
|
| 487 |
+
with warnings.catch_warnings(record=True) as ws:
|
| 488 |
+
if input_lines[0].endswith(';'):
|
| 489 |
+
is_semicolon = True
|
| 490 |
+
#for i, line in enumerate(input_lines):
|
| 491 |
+
|
| 492 |
+
# process the first input line
|
| 493 |
+
if is_verbatim:
|
| 494 |
+
self.process_input_lines([''])
|
| 495 |
+
self.IP.execution_count += 1 # increment it anyway
|
| 496 |
+
else:
|
| 497 |
+
# only submit the line in non-verbatim mode
|
| 498 |
+
self.process_input_lines(input_lines, store_history=store_history)
|
| 499 |
+
|
| 500 |
+
if not is_suppress:
|
| 501 |
+
for i, line in enumerate(input_lines):
|
| 502 |
+
if i == 0:
|
| 503 |
+
formatted_line = '%s %s'%(input_prompt, line)
|
| 504 |
+
else:
|
| 505 |
+
formatted_line = '%s %s'%(continuation, line)
|
| 506 |
+
ret.append(formatted_line)
|
| 507 |
+
|
| 508 |
+
if not is_suppress and len(rest.strip()) and is_verbatim:
|
| 509 |
+
# The "rest" is the standard output of the input. This needs to be
|
| 510 |
+
# added when in verbatim mode. If there is no "rest", then we don't
|
| 511 |
+
# add it, as the new line will be added by the processed output.
|
| 512 |
+
ret.append(rest)
|
| 513 |
+
|
| 514 |
+
# Fetch the processed output. (This is not the submitted output.)
|
| 515 |
+
self.cout.seek(0)
|
| 516 |
+
processed_output = self.cout.read()
|
| 517 |
+
if not is_suppress and not is_semicolon:
|
| 518 |
+
#
|
| 519 |
+
# In IPythonDirective.run, the elements of `ret` are eventually
|
| 520 |
+
# combined such that '' entries correspond to newlines. So if
|
| 521 |
+
# `processed_output` is equal to '', then the adding it to `ret`
|
| 522 |
+
# ensures that there is a blank line between consecutive inputs
|
| 523 |
+
# that have no outputs, as in:
|
| 524 |
+
#
|
| 525 |
+
# In [1]: x = 4
|
| 526 |
+
#
|
| 527 |
+
# In [2]: x = 5
|
| 528 |
+
#
|
| 529 |
+
# When there is processed output, it has a '\n' at the tail end. So
|
| 530 |
+
# adding the output to `ret` will provide the necessary spacing
|
| 531 |
+
# between consecutive input/output blocks, as in:
|
| 532 |
+
#
|
| 533 |
+
# In [1]: x
|
| 534 |
+
# Out[1]: 5
|
| 535 |
+
#
|
| 536 |
+
# In [2]: x
|
| 537 |
+
# Out[2]: 5
|
| 538 |
+
#
|
| 539 |
+
# When there is stdout from the input, it also has a '\n' at the
|
| 540 |
+
# tail end, and so this ensures proper spacing as well. E.g.:
|
| 541 |
+
#
|
| 542 |
+
# In [1]: print(x)
|
| 543 |
+
# 5
|
| 544 |
+
#
|
| 545 |
+
# In [2]: x = 5
|
| 546 |
+
#
|
| 547 |
+
# When in verbatim mode, `processed_output` is empty (because
|
| 548 |
+
# nothing was passed to IP. Sometimes the submitted code block has
|
| 549 |
+
# an Out[] portion and sometimes it does not. When it does not, we
|
| 550 |
+
# need to ensure proper spacing, so we have to add '' to `ret`.
|
| 551 |
+
# However, if there is an Out[] in the submitted code, then we do
|
| 552 |
+
# not want to add a newline as `process_output` has stuff to add.
|
| 553 |
+
# The difficulty is that `process_input` doesn't know if
|
| 554 |
+
# `process_output` will be called---so it doesn't know if there is
|
| 555 |
+
# Out[] in the code block. The requires that we include a hack in
|
| 556 |
+
# `process_block`. See the comments there.
|
| 557 |
+
#
|
| 558 |
+
ret.append(processed_output)
|
| 559 |
+
elif is_semicolon:
|
| 560 |
+
# Make sure there is a newline after the semicolon.
|
| 561 |
+
ret.append('')
|
| 562 |
+
|
| 563 |
+
# context information
|
| 564 |
+
filename = "Unknown"
|
| 565 |
+
lineno = 0
|
| 566 |
+
if self.directive.state:
|
| 567 |
+
filename = self.directive.state.document.current_source
|
| 568 |
+
lineno = self.directive.state.document.current_line
|
| 569 |
+
|
| 570 |
+
# Use sphinx logger for warnings
|
| 571 |
+
logger = logging.getLogger(__name__)
|
| 572 |
+
|
| 573 |
+
# output any exceptions raised during execution to stdout
|
| 574 |
+
# unless :okexcept: has been specified.
|
| 575 |
+
if not is_okexcept and (
|
| 576 |
+
("Traceback" in processed_output) or ("SyntaxError" in processed_output)
|
| 577 |
+
):
|
| 578 |
+
s = "\n>>>" + ("-" * 73) + "\n"
|
| 579 |
+
s += "Exception in %s at block ending on line %s\n" % (filename, lineno)
|
| 580 |
+
s += "Specify :okexcept: as an option in the ipython:: block to suppress this message\n"
|
| 581 |
+
s += processed_output + "\n"
|
| 582 |
+
s += "<<<" + ("-" * 73)
|
| 583 |
+
logger.warning(s)
|
| 584 |
+
if self.warning_is_error:
|
| 585 |
+
raise RuntimeError(
|
| 586 |
+
"Unexpected exception in `{}` line {}".format(filename, lineno)
|
| 587 |
+
)
|
| 588 |
+
|
| 589 |
+
# output any warning raised during execution to stdout
|
| 590 |
+
# unless :okwarning: has been specified.
|
| 591 |
+
if not is_okwarning:
|
| 592 |
+
for w in ws:
|
| 593 |
+
s = "\n>>>" + ("-" * 73) + "\n"
|
| 594 |
+
s += "Warning in %s at block ending on line %s\n" % (filename, lineno)
|
| 595 |
+
s += "Specify :okwarning: as an option in the ipython:: block to suppress this message\n"
|
| 596 |
+
s += ("-" * 76) + "\n"
|
| 597 |
+
s += warnings.formatwarning(
|
| 598 |
+
w.message, w.category, w.filename, w.lineno, w.line
|
| 599 |
+
)
|
| 600 |
+
s += "<<<" + ("-" * 73)
|
| 601 |
+
logger.warning(s)
|
| 602 |
+
if self.warning_is_error:
|
| 603 |
+
raise RuntimeError(
|
| 604 |
+
"Unexpected warning in `{}` line {}".format(filename, lineno)
|
| 605 |
+
)
|
| 606 |
+
|
| 607 |
+
self.clear_cout()
|
| 608 |
+
return (ret, input_lines, processed_output,
|
| 609 |
+
is_doctest, decorator, image_file, image_directive)
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def process_output(self, data, output_prompt, input_lines, output,
|
| 613 |
+
is_doctest, decorator, image_file):
|
| 614 |
+
"""
|
| 615 |
+
Process data block for OUTPUT token.
|
| 616 |
+
|
| 617 |
+
"""
|
| 618 |
+
# Recall: `data` is the submitted output, and `output` is the processed
|
| 619 |
+
# output from `input_lines`.
|
| 620 |
+
|
| 621 |
+
TAB = ' ' * 4
|
| 622 |
+
|
| 623 |
+
if is_doctest and output is not None:
|
| 624 |
+
|
| 625 |
+
found = output # This is the processed output
|
| 626 |
+
found = found.strip()
|
| 627 |
+
submitted = data.strip()
|
| 628 |
+
|
| 629 |
+
if self.directive is None:
|
| 630 |
+
source = 'Unavailable'
|
| 631 |
+
content = 'Unavailable'
|
| 632 |
+
else:
|
| 633 |
+
source = self.directive.state.document.current_source
|
| 634 |
+
content = self.directive.content
|
| 635 |
+
# Add tabs and join into a single string.
|
| 636 |
+
content = '\n'.join([TAB + line for line in content])
|
| 637 |
+
|
| 638 |
+
# Make sure the output contains the output prompt.
|
| 639 |
+
ind = found.find(output_prompt)
|
| 640 |
+
if ind < 0:
|
| 641 |
+
e = ('output does not contain output prompt\n\n'
|
| 642 |
+
'Document source: {0}\n\n'
|
| 643 |
+
'Raw content: \n{1}\n\n'
|
| 644 |
+
'Input line(s):\n{TAB}{2}\n\n'
|
| 645 |
+
'Output line(s):\n{TAB}{3}\n\n')
|
| 646 |
+
e = e.format(source, content, '\n'.join(input_lines),
|
| 647 |
+
repr(found), TAB=TAB)
|
| 648 |
+
raise RuntimeError(e)
|
| 649 |
+
found = found[len(output_prompt):].strip()
|
| 650 |
+
|
| 651 |
+
# Handle the actual doctest comparison.
|
| 652 |
+
if decorator.strip() == '@doctest':
|
| 653 |
+
# Standard doctest
|
| 654 |
+
if found != submitted:
|
| 655 |
+
e = ('doctest failure\n\n'
|
| 656 |
+
'Document source: {0}\n\n'
|
| 657 |
+
'Raw content: \n{1}\n\n'
|
| 658 |
+
'On input line(s):\n{TAB}{2}\n\n'
|
| 659 |
+
'we found output:\n{TAB}{3}\n\n'
|
| 660 |
+
'instead of the expected:\n{TAB}{4}\n\n')
|
| 661 |
+
e = e.format(source, content, '\n'.join(input_lines),
|
| 662 |
+
repr(found), repr(submitted), TAB=TAB)
|
| 663 |
+
raise RuntimeError(e)
|
| 664 |
+
else:
|
| 665 |
+
self.custom_doctest(decorator, input_lines, found, submitted)
|
| 666 |
+
|
| 667 |
+
# When in verbatim mode, this holds additional submitted output
|
| 668 |
+
# to be written in the final Sphinx output.
|
| 669 |
+
# https://github.com/ipython/ipython/issues/5776
|
| 670 |
+
out_data = []
|
| 671 |
+
|
| 672 |
+
is_verbatim = decorator=='@verbatim' or self.is_verbatim
|
| 673 |
+
if is_verbatim and data.strip():
|
| 674 |
+
# Note that `ret` in `process_block` has '' as its last element if
|
| 675 |
+
# the code block was in verbatim mode. So if there is no submitted
|
| 676 |
+
# output, then we will have proper spacing only if we do not add
|
| 677 |
+
# an additional '' to `out_data`. This is why we condition on
|
| 678 |
+
# `and data.strip()`.
|
| 679 |
+
|
| 680 |
+
# The submitted output has no output prompt. If we want the
|
| 681 |
+
# prompt and the code to appear, we need to join them now
|
| 682 |
+
# instead of adding them separately---as this would create an
|
| 683 |
+
# undesired newline. How we do this ultimately depends on the
|
| 684 |
+
# format of the output regex. I'll do what works for the default
|
| 685 |
+
# prompt for now, and we might have to adjust if it doesn't work
|
| 686 |
+
# in other cases. Finally, the submitted output does not have
|
| 687 |
+
# a trailing newline, so we must add it manually.
|
| 688 |
+
out_data.append("{0} {1}\n".format(output_prompt, data))
|
| 689 |
+
|
| 690 |
+
return out_data
|
| 691 |
+
|
| 692 |
+
def process_comment(self, data):
|
| 693 |
+
"""Process data fPblock for COMMENT token."""
|
| 694 |
+
if not self.is_suppress:
|
| 695 |
+
return [data]
|
| 696 |
+
|
| 697 |
+
def save_image(self, image_file):
|
| 698 |
+
"""
|
| 699 |
+
Saves the image file to disk.
|
| 700 |
+
"""
|
| 701 |
+
self.ensure_pyplot()
|
| 702 |
+
command = 'plt.gcf().savefig("%s")'%image_file
|
| 703 |
+
# print('SAVEFIG', command) # dbg
|
| 704 |
+
self.process_input_line('bookmark ipy_thisdir', store_history=False)
|
| 705 |
+
self.process_input_line('cd -b ipy_savedir', store_history=False)
|
| 706 |
+
self.process_input_line(command, store_history=False)
|
| 707 |
+
self.process_input_line('cd -b ipy_thisdir', store_history=False)
|
| 708 |
+
self.process_input_line('bookmark -d ipy_thisdir', store_history=False)
|
| 709 |
+
self.clear_cout()
|
| 710 |
+
|
| 711 |
+
def process_block(self, block):
|
| 712 |
+
"""
|
| 713 |
+
process block from the block_parser and return a list of processed lines
|
| 714 |
+
"""
|
| 715 |
+
ret = []
|
| 716 |
+
output = None
|
| 717 |
+
input_lines = None
|
| 718 |
+
lineno = self.IP.execution_count
|
| 719 |
+
|
| 720 |
+
input_prompt = self.promptin % lineno
|
| 721 |
+
output_prompt = self.promptout % lineno
|
| 722 |
+
image_file = None
|
| 723 |
+
image_directive = None
|
| 724 |
+
|
| 725 |
+
found_input = False
|
| 726 |
+
for token, data in block:
|
| 727 |
+
if token == COMMENT:
|
| 728 |
+
out_data = self.process_comment(data)
|
| 729 |
+
elif token == INPUT:
|
| 730 |
+
found_input = True
|
| 731 |
+
(out_data, input_lines, output, is_doctest,
|
| 732 |
+
decorator, image_file, image_directive) = \
|
| 733 |
+
self.process_input(data, input_prompt, lineno)
|
| 734 |
+
elif token == OUTPUT:
|
| 735 |
+
if not found_input:
|
| 736 |
+
|
| 737 |
+
TAB = ' ' * 4
|
| 738 |
+
linenumber = 0
|
| 739 |
+
source = 'Unavailable'
|
| 740 |
+
content = 'Unavailable'
|
| 741 |
+
if self.directive:
|
| 742 |
+
linenumber = self.directive.state.document.current_line
|
| 743 |
+
source = self.directive.state.document.current_source
|
| 744 |
+
content = self.directive.content
|
| 745 |
+
# Add tabs and join into a single string.
|
| 746 |
+
content = '\n'.join([TAB + line for line in content])
|
| 747 |
+
|
| 748 |
+
e = ('\n\nInvalid block: Block contains an output prompt '
|
| 749 |
+
'without an input prompt.\n\n'
|
| 750 |
+
'Document source: {0}\n\n'
|
| 751 |
+
'Content begins at line {1}: \n\n{2}\n\n'
|
| 752 |
+
'Problematic block within content: \n\n{TAB}{3}\n\n')
|
| 753 |
+
e = e.format(source, linenumber, content, block, TAB=TAB)
|
| 754 |
+
|
| 755 |
+
# Write, rather than include in exception, since Sphinx
|
| 756 |
+
# will truncate tracebacks.
|
| 757 |
+
sys.stdout.write(e)
|
| 758 |
+
raise RuntimeError('An invalid block was detected.')
|
| 759 |
+
out_data = \
|
| 760 |
+
self.process_output(data, output_prompt, input_lines,
|
| 761 |
+
output, is_doctest, decorator,
|
| 762 |
+
image_file)
|
| 763 |
+
if out_data:
|
| 764 |
+
# Then there was user submitted output in verbatim mode.
|
| 765 |
+
# We need to remove the last element of `ret` that was
|
| 766 |
+
# added in `process_input`, as it is '' and would introduce
|
| 767 |
+
# an undesirable newline.
|
| 768 |
+
assert(ret[-1] == '')
|
| 769 |
+
del ret[-1]
|
| 770 |
+
|
| 771 |
+
if out_data:
|
| 772 |
+
ret.extend(out_data)
|
| 773 |
+
|
| 774 |
+
# save the image files
|
| 775 |
+
if image_file is not None:
|
| 776 |
+
self.save_image(image_file)
|
| 777 |
+
|
| 778 |
+
return ret, image_directive
|
| 779 |
+
|
| 780 |
+
def ensure_pyplot(self):
|
| 781 |
+
"""
|
| 782 |
+
Ensures that pyplot has been imported into the embedded IPython shell.
|
| 783 |
+
|
| 784 |
+
Also, makes sure to set the backend appropriately if not set already.
|
| 785 |
+
|
| 786 |
+
"""
|
| 787 |
+
# We are here if the @figure pseudo decorator was used. Thus, it's
|
| 788 |
+
# possible that we could be here even if python_mplbackend were set to
|
| 789 |
+
# `None`. That's also strange and perhaps worthy of raising an
|
| 790 |
+
# exception, but for now, we just set the backend to 'agg'.
|
| 791 |
+
|
| 792 |
+
if not self._pyplot_imported:
|
| 793 |
+
if 'matplotlib.backends' not in sys.modules:
|
| 794 |
+
# Then ipython_matplotlib was set to None but there was a
|
| 795 |
+
# call to the @figure decorator (and ipython_execlines did
|
| 796 |
+
# not set a backend).
|
| 797 |
+
#raise Exception("No backend was set, but @figure was used!")
|
| 798 |
+
import matplotlib
|
| 799 |
+
matplotlib.use('agg')
|
| 800 |
+
|
| 801 |
+
# Always import pyplot into embedded shell.
|
| 802 |
+
self.process_input_line('import matplotlib.pyplot as plt',
|
| 803 |
+
store_history=False)
|
| 804 |
+
self._pyplot_imported = True
|
| 805 |
+
|
| 806 |
+
def process_pure_python(self, content):
|
| 807 |
+
"""
|
| 808 |
+
content is a list of strings. it is unedited directive content
|
| 809 |
+
|
| 810 |
+
This runs it line by line in the InteractiveShell, prepends
|
| 811 |
+
prompts as needed capturing stderr and stdout, then returns
|
| 812 |
+
the content as a list as if it were ipython code
|
| 813 |
+
"""
|
| 814 |
+
output = []
|
| 815 |
+
savefig = False # keep up with this to clear figure
|
| 816 |
+
multiline = False # to handle line continuation
|
| 817 |
+
multiline_start = None
|
| 818 |
+
fmtin = self.promptin
|
| 819 |
+
|
| 820 |
+
ct = 0
|
| 821 |
+
|
| 822 |
+
for lineno, line in enumerate(content):
|
| 823 |
+
|
| 824 |
+
line_stripped = line.strip()
|
| 825 |
+
if not len(line):
|
| 826 |
+
output.append(line)
|
| 827 |
+
continue
|
| 828 |
+
|
| 829 |
+
# handle pseudo-decorators, whilst ensuring real python decorators are treated as input
|
| 830 |
+
if any(
|
| 831 |
+
line_stripped.startswith("@" + pseudo_decorator)
|
| 832 |
+
for pseudo_decorator in PSEUDO_DECORATORS
|
| 833 |
+
):
|
| 834 |
+
output.extend([line])
|
| 835 |
+
if 'savefig' in line:
|
| 836 |
+
savefig = True # and need to clear figure
|
| 837 |
+
continue
|
| 838 |
+
|
| 839 |
+
# handle comments
|
| 840 |
+
if line_stripped.startswith('#'):
|
| 841 |
+
output.extend([line])
|
| 842 |
+
continue
|
| 843 |
+
|
| 844 |
+
# deal with lines checking for multiline
|
| 845 |
+
continuation = u' %s:'% ''.join(['.']*(len(str(ct))+2))
|
| 846 |
+
if not multiline:
|
| 847 |
+
modified = u"%s %s" % (fmtin % ct, line_stripped)
|
| 848 |
+
output.append(modified)
|
| 849 |
+
ct += 1
|
| 850 |
+
try:
|
| 851 |
+
ast.parse(line_stripped)
|
| 852 |
+
output.append(u'')
|
| 853 |
+
except Exception: # on a multiline
|
| 854 |
+
multiline = True
|
| 855 |
+
multiline_start = lineno
|
| 856 |
+
else: # still on a multiline
|
| 857 |
+
modified = u'%s %s' % (continuation, line)
|
| 858 |
+
output.append(modified)
|
| 859 |
+
|
| 860 |
+
# if the next line is indented, it should be part of multiline
|
| 861 |
+
if len(content) > lineno + 1:
|
| 862 |
+
nextline = content[lineno + 1]
|
| 863 |
+
if len(nextline) - len(nextline.lstrip()) > 3:
|
| 864 |
+
continue
|
| 865 |
+
try:
|
| 866 |
+
mod = ast.parse(
|
| 867 |
+
'\n'.join(content[multiline_start:lineno+1]))
|
| 868 |
+
if isinstance(mod.body[0], ast.FunctionDef):
|
| 869 |
+
# check to see if we have the whole function
|
| 870 |
+
for element in mod.body[0].body:
|
| 871 |
+
if isinstance(element, ast.Return):
|
| 872 |
+
multiline = False
|
| 873 |
+
else:
|
| 874 |
+
output.append(u'')
|
| 875 |
+
multiline = False
|
| 876 |
+
except Exception:
|
| 877 |
+
pass
|
| 878 |
+
|
| 879 |
+
if savefig: # clear figure if plotted
|
| 880 |
+
self.ensure_pyplot()
|
| 881 |
+
self.process_input_line('plt.clf()', store_history=False)
|
| 882 |
+
self.clear_cout()
|
| 883 |
+
savefig = False
|
| 884 |
+
|
| 885 |
+
return output
|
| 886 |
+
|
| 887 |
+
def custom_doctest(self, decorator, input_lines, found, submitted):
|
| 888 |
+
"""
|
| 889 |
+
Perform a specialized doctest.
|
| 890 |
+
|
| 891 |
+
"""
|
| 892 |
+
from .custom_doctests import doctests
|
| 893 |
+
|
| 894 |
+
args = decorator.split()
|
| 895 |
+
doctest_type = args[1]
|
| 896 |
+
if doctest_type in doctests:
|
| 897 |
+
doctests[doctest_type](self, args, input_lines, found, submitted)
|
| 898 |
+
else:
|
| 899 |
+
e = "Invalid option to @doctest: {0}".format(doctest_type)
|
| 900 |
+
raise Exception(e)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
class IPythonDirective(Directive):
|
| 904 |
+
|
| 905 |
+
has_content: bool = True
|
| 906 |
+
required_arguments: int = 0
|
| 907 |
+
optional_arguments: int = 4 # python, suppress, verbatim, doctest
|
| 908 |
+
final_argumuent_whitespace: bool = True
|
| 909 |
+
option_spec: Dict[str, Any] = {
|
| 910 |
+
"python": directives.unchanged,
|
| 911 |
+
"suppress": directives.flag,
|
| 912 |
+
"verbatim": directives.flag,
|
| 913 |
+
"doctest": directives.flag,
|
| 914 |
+
"okexcept": directives.flag,
|
| 915 |
+
"okwarning": directives.flag,
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
shell = None
|
| 919 |
+
|
| 920 |
+
seen_docs: Set = set()
|
| 921 |
+
|
| 922 |
+
def get_config_options(self):
|
| 923 |
+
# contains sphinx configuration variables
|
| 924 |
+
config = self.state.document.settings.env.config
|
| 925 |
+
|
| 926 |
+
# get config variables to set figure output directory
|
| 927 |
+
savefig_dir = config.ipython_savefig_dir
|
| 928 |
+
source_dir = self.state.document.settings.env.srcdir
|
| 929 |
+
savefig_dir = os.path.join(source_dir, savefig_dir)
|
| 930 |
+
|
| 931 |
+
# get regex and prompt stuff
|
| 932 |
+
rgxin = config.ipython_rgxin
|
| 933 |
+
rgxout = config.ipython_rgxout
|
| 934 |
+
warning_is_error= config.ipython_warning_is_error
|
| 935 |
+
promptin = config.ipython_promptin
|
| 936 |
+
promptout = config.ipython_promptout
|
| 937 |
+
mplbackend = config.ipython_mplbackend
|
| 938 |
+
exec_lines = config.ipython_execlines
|
| 939 |
+
hold_count = config.ipython_holdcount
|
| 940 |
+
|
| 941 |
+
return (savefig_dir, source_dir, rgxin, rgxout,
|
| 942 |
+
promptin, promptout, mplbackend, exec_lines, hold_count, warning_is_error)
|
| 943 |
+
|
| 944 |
+
def setup(self):
|
| 945 |
+
# Get configuration values.
|
| 946 |
+
(savefig_dir, source_dir, rgxin, rgxout, promptin, promptout,
|
| 947 |
+
mplbackend, exec_lines, hold_count, warning_is_error) = self.get_config_options()
|
| 948 |
+
|
| 949 |
+
try:
|
| 950 |
+
os.makedirs(savefig_dir)
|
| 951 |
+
except OSError as e:
|
| 952 |
+
if e.errno != errno.EEXIST:
|
| 953 |
+
raise
|
| 954 |
+
|
| 955 |
+
if self.shell is None:
|
| 956 |
+
# We will be here many times. However, when the
|
| 957 |
+
# EmbeddedSphinxShell is created, its interactive shell member
|
| 958 |
+
# is the same for each instance.
|
| 959 |
+
|
| 960 |
+
if mplbackend and 'matplotlib.backends' not in sys.modules and use_matplotlib:
|
| 961 |
+
import matplotlib
|
| 962 |
+
matplotlib.use(mplbackend)
|
| 963 |
+
|
| 964 |
+
# Must be called after (potentially) importing matplotlib and
|
| 965 |
+
# setting its backend since exec_lines might import pylab.
|
| 966 |
+
self.shell = EmbeddedSphinxShell(exec_lines)
|
| 967 |
+
|
| 968 |
+
# Store IPython directive to enable better error messages
|
| 969 |
+
self.shell.directive = self
|
| 970 |
+
|
| 971 |
+
# reset the execution count if we haven't processed this doc
|
| 972 |
+
#NOTE: this may be borked if there are multiple seen_doc tmp files
|
| 973 |
+
#check time stamp?
|
| 974 |
+
if self.state.document.current_source not in self.seen_docs:
|
| 975 |
+
self.shell.IP.history_manager.reset()
|
| 976 |
+
self.shell.IP.execution_count = 1
|
| 977 |
+
self.seen_docs.add(self.state.document.current_source)
|
| 978 |
+
|
| 979 |
+
# and attach to shell so we don't have to pass them around
|
| 980 |
+
self.shell.rgxin = rgxin
|
| 981 |
+
self.shell.rgxout = rgxout
|
| 982 |
+
self.shell.promptin = promptin
|
| 983 |
+
self.shell.promptout = promptout
|
| 984 |
+
self.shell.savefig_dir = savefig_dir
|
| 985 |
+
self.shell.source_dir = source_dir
|
| 986 |
+
self.shell.hold_count = hold_count
|
| 987 |
+
self.shell.warning_is_error = warning_is_error
|
| 988 |
+
|
| 989 |
+
# setup bookmark for saving figures directory
|
| 990 |
+
self.shell.process_input_line(
|
| 991 |
+
'bookmark ipy_savedir "%s"' % savefig_dir, store_history=False
|
| 992 |
+
)
|
| 993 |
+
self.shell.clear_cout()
|
| 994 |
+
|
| 995 |
+
return rgxin, rgxout, promptin, promptout
|
| 996 |
+
|
| 997 |
+
def teardown(self):
|
| 998 |
+
# delete last bookmark
|
| 999 |
+
self.shell.process_input_line('bookmark -d ipy_savedir',
|
| 1000 |
+
store_history=False)
|
| 1001 |
+
self.shell.clear_cout()
|
| 1002 |
+
|
| 1003 |
+
def run(self):
|
| 1004 |
+
debug = False
|
| 1005 |
+
|
| 1006 |
+
#TODO, any reason block_parser can't be a method of embeddable shell
|
| 1007 |
+
# then we wouldn't have to carry these around
|
| 1008 |
+
rgxin, rgxout, promptin, promptout = self.setup()
|
| 1009 |
+
|
| 1010 |
+
options = self.options
|
| 1011 |
+
self.shell.is_suppress = 'suppress' in options
|
| 1012 |
+
self.shell.is_doctest = 'doctest' in options
|
| 1013 |
+
self.shell.is_verbatim = 'verbatim' in options
|
| 1014 |
+
self.shell.is_okexcept = 'okexcept' in options
|
| 1015 |
+
self.shell.is_okwarning = 'okwarning' in options
|
| 1016 |
+
|
| 1017 |
+
# handle pure python code
|
| 1018 |
+
if 'python' in self.arguments:
|
| 1019 |
+
content = self.content
|
| 1020 |
+
self.content = self.shell.process_pure_python(content)
|
| 1021 |
+
|
| 1022 |
+
# parts consists of all text within the ipython-block.
|
| 1023 |
+
# Each part is an input/output block.
|
| 1024 |
+
parts = '\n'.join(self.content).split('\n\n')
|
| 1025 |
+
|
| 1026 |
+
lines = ['.. code-block:: ipython', '']
|
| 1027 |
+
figures = []
|
| 1028 |
+
|
| 1029 |
+
# Use sphinx logger for warnings
|
| 1030 |
+
logger = logging.getLogger(__name__)
|
| 1031 |
+
|
| 1032 |
+
for part in parts:
|
| 1033 |
+
block = block_parser(part, rgxin, rgxout, promptin, promptout)
|
| 1034 |
+
if len(block):
|
| 1035 |
+
rows, figure = self.shell.process_block(block)
|
| 1036 |
+
for row in rows:
|
| 1037 |
+
lines.extend([' {0}'.format(line)
|
| 1038 |
+
for line in row.split('\n')])
|
| 1039 |
+
|
| 1040 |
+
if figure is not None:
|
| 1041 |
+
figures.append(figure)
|
| 1042 |
+
else:
|
| 1043 |
+
message = 'Code input with no code at {}, line {}'\
|
| 1044 |
+
.format(
|
| 1045 |
+
self.state.document.current_source,
|
| 1046 |
+
self.state.document.current_line)
|
| 1047 |
+
if self.shell.warning_is_error:
|
| 1048 |
+
raise RuntimeError(message)
|
| 1049 |
+
else:
|
| 1050 |
+
logger.warning(message)
|
| 1051 |
+
|
| 1052 |
+
for figure in figures:
|
| 1053 |
+
lines.append('')
|
| 1054 |
+
lines.extend(figure.split('\n'))
|
| 1055 |
+
lines.append('')
|
| 1056 |
+
|
| 1057 |
+
if len(lines) > 2:
|
| 1058 |
+
if debug:
|
| 1059 |
+
print('\n'.join(lines))
|
| 1060 |
+
else:
|
| 1061 |
+
# This has to do with input, not output. But if we comment
|
| 1062 |
+
# these lines out, then no IPython code will appear in the
|
| 1063 |
+
# final output.
|
| 1064 |
+
self.state_machine.insert_input(
|
| 1065 |
+
lines, self.state_machine.input_lines.source(0))
|
| 1066 |
+
|
| 1067 |
+
# cleanup
|
| 1068 |
+
self.teardown()
|
| 1069 |
+
|
| 1070 |
+
return []
|
| 1071 |
+
|
| 1072 |
+
# Enable as a proper Sphinx directive
|
| 1073 |
+
def setup(app):
|
| 1074 |
+
setup.app = app
|
| 1075 |
+
|
| 1076 |
+
app.add_directive('ipython', IPythonDirective)
|
| 1077 |
+
app.add_config_value('ipython_savefig_dir', 'savefig', 'env')
|
| 1078 |
+
app.add_config_value('ipython_warning_is_error', True, 'env')
|
| 1079 |
+
app.add_config_value('ipython_rgxin',
|
| 1080 |
+
re.compile(r'In \[(\d+)\]:\s?(.*)\s*'), 'env')
|
| 1081 |
+
app.add_config_value('ipython_rgxout',
|
| 1082 |
+
re.compile(r'Out\[(\d+)\]:\s?(.*)\s*'), 'env')
|
| 1083 |
+
app.add_config_value('ipython_promptin', 'In [%d]:', 'env')
|
| 1084 |
+
app.add_config_value('ipython_promptout', 'Out[%d]:', 'env')
|
| 1085 |
+
|
| 1086 |
+
# We could just let matplotlib pick whatever is specified as the default
|
| 1087 |
+
# backend in the matplotlibrc file, but this would cause issues if the
|
| 1088 |
+
# backend didn't work in headless environments. For this reason, 'agg'
|
| 1089 |
+
# is a good default backend choice.
|
| 1090 |
+
app.add_config_value('ipython_mplbackend', 'agg', 'env')
|
| 1091 |
+
|
| 1092 |
+
# If the user sets this config value to `None`, then EmbeddedSphinxShell's
|
| 1093 |
+
# __init__ method will treat it as [].
|
| 1094 |
+
execlines = ['import numpy as np']
|
| 1095 |
+
if use_matplotlib:
|
| 1096 |
+
execlines.append('import matplotlib.pyplot as plt')
|
| 1097 |
+
app.add_config_value('ipython_execlines', execlines, 'env')
|
| 1098 |
+
|
| 1099 |
+
app.add_config_value('ipython_holdcount', True, 'env')
|
| 1100 |
+
|
| 1101 |
+
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
|
| 1102 |
+
return metadata
|
| 1103 |
+
|
| 1104 |
+
# Simple smoke test, needs to be converted to a proper automatic test.
|
| 1105 |
+
def test():
|
| 1106 |
+
|
| 1107 |
+
examples = [
|
| 1108 |
+
r"""
|
| 1109 |
+
In [9]: pwd
|
| 1110 |
+
Out[9]: '/home/jdhunter/py4science/book'
|
| 1111 |
+
|
| 1112 |
+
In [10]: cd bookdata/
|
| 1113 |
+
/home/jdhunter/py4science/book/bookdata
|
| 1114 |
+
|
| 1115 |
+
In [2]: from pylab import *
|
| 1116 |
+
|
| 1117 |
+
In [2]: ion()
|
| 1118 |
+
|
| 1119 |
+
In [3]: im = imread('stinkbug.png')
|
| 1120 |
+
|
| 1121 |
+
@savefig mystinkbug.png width=4in
|
| 1122 |
+
In [4]: imshow(im)
|
| 1123 |
+
Out[4]: <matplotlib.image.AxesImage object at 0x39ea850>
|
| 1124 |
+
|
| 1125 |
+
""",
|
| 1126 |
+
r"""
|
| 1127 |
+
|
| 1128 |
+
In [1]: x = 'hello world'
|
| 1129 |
+
|
| 1130 |
+
# string methods can be
|
| 1131 |
+
# used to alter the string
|
| 1132 |
+
@doctest
|
| 1133 |
+
In [2]: x.upper()
|
| 1134 |
+
Out[2]: 'HELLO WORLD'
|
| 1135 |
+
|
| 1136 |
+
@verbatim
|
| 1137 |
+
In [3]: x.st<TAB>
|
| 1138 |
+
x.startswith x.strip
|
| 1139 |
+
""",
|
| 1140 |
+
r"""
|
| 1141 |
+
|
| 1142 |
+
In [130]: url = 'http://ichart.finance.yahoo.com/table.csv?s=CROX\
|
| 1143 |
+
.....: &d=9&e=22&f=2009&g=d&a=1&br=8&c=2006&ignore=.csv'
|
| 1144 |
+
|
| 1145 |
+
In [131]: print url.split('&')
|
| 1146 |
+
['http://ichart.finance.yahoo.com/table.csv?s=CROX', 'd=9', 'e=22', 'f=2009', 'g=d', 'a=1', 'b=8', 'c=2006', 'ignore=.csv']
|
| 1147 |
+
|
| 1148 |
+
In [60]: import urllib
|
| 1149 |
+
|
| 1150 |
+
""",
|
| 1151 |
+
r"""\
|
| 1152 |
+
|
| 1153 |
+
In [133]: import numpy.random
|
| 1154 |
+
|
| 1155 |
+
@suppress
|
| 1156 |
+
In [134]: numpy.random.seed(2358)
|
| 1157 |
+
|
| 1158 |
+
@doctest
|
| 1159 |
+
In [135]: numpy.random.rand(10,2)
|
| 1160 |
+
Out[135]:
|
| 1161 |
+
array([[ 0.64524308, 0.59943846],
|
| 1162 |
+
[ 0.47102322, 0.8715456 ],
|
| 1163 |
+
[ 0.29370834, 0.74776844],
|
| 1164 |
+
[ 0.99539577, 0.1313423 ],
|
| 1165 |
+
[ 0.16250302, 0.21103583],
|
| 1166 |
+
[ 0.81626524, 0.1312433 ],
|
| 1167 |
+
[ 0.67338089, 0.72302393],
|
| 1168 |
+
[ 0.7566368 , 0.07033696],
|
| 1169 |
+
[ 0.22591016, 0.77731835],
|
| 1170 |
+
[ 0.0072729 , 0.34273127]])
|
| 1171 |
+
|
| 1172 |
+
""",
|
| 1173 |
+
|
| 1174 |
+
r"""
|
| 1175 |
+
In [106]: print x
|
| 1176 |
+
jdh
|
| 1177 |
+
|
| 1178 |
+
In [109]: for i in range(10):
|
| 1179 |
+
.....: print i
|
| 1180 |
+
.....:
|
| 1181 |
+
.....:
|
| 1182 |
+
0
|
| 1183 |
+
1
|
| 1184 |
+
2
|
| 1185 |
+
3
|
| 1186 |
+
4
|
| 1187 |
+
5
|
| 1188 |
+
6
|
| 1189 |
+
7
|
| 1190 |
+
8
|
| 1191 |
+
9
|
| 1192 |
+
""",
|
| 1193 |
+
|
| 1194 |
+
r"""
|
| 1195 |
+
|
| 1196 |
+
In [144]: from pylab import *
|
| 1197 |
+
|
| 1198 |
+
In [145]: ion()
|
| 1199 |
+
|
| 1200 |
+
# use a semicolon to suppress the output
|
| 1201 |
+
@savefig test_hist.png width=4in
|
| 1202 |
+
In [151]: hist(np.random.randn(10000), 100);
|
| 1203 |
+
|
| 1204 |
+
|
| 1205 |
+
@savefig test_plot.png width=4in
|
| 1206 |
+
In [151]: plot(np.random.randn(10000), 'o');
|
| 1207 |
+
""",
|
| 1208 |
+
|
| 1209 |
+
r"""
|
| 1210 |
+
# use a semicolon to suppress the output
|
| 1211 |
+
In [151]: plt.clf()
|
| 1212 |
+
|
| 1213 |
+
@savefig plot_simple.png width=4in
|
| 1214 |
+
In [151]: plot([1,2,3])
|
| 1215 |
+
|
| 1216 |
+
@savefig hist_simple.png width=4in
|
| 1217 |
+
In [151]: hist(np.random.randn(10000), 100);
|
| 1218 |
+
|
| 1219 |
+
""",
|
| 1220 |
+
r"""
|
| 1221 |
+
# update the current fig
|
| 1222 |
+
In [151]: ylabel('number')
|
| 1223 |
+
|
| 1224 |
+
In [152]: title('normal distribution')
|
| 1225 |
+
|
| 1226 |
+
|
| 1227 |
+
@savefig hist_with_text.png
|
| 1228 |
+
In [153]: grid(True)
|
| 1229 |
+
|
| 1230 |
+
@doctest float
|
| 1231 |
+
In [154]: 0.1 + 0.2
|
| 1232 |
+
Out[154]: 0.3
|
| 1233 |
+
|
| 1234 |
+
@doctest float
|
| 1235 |
+
In [155]: np.arange(16).reshape(4,4)
|
| 1236 |
+
Out[155]:
|
| 1237 |
+
array([[ 0, 1, 2, 3],
|
| 1238 |
+
[ 4, 5, 6, 7],
|
| 1239 |
+
[ 8, 9, 10, 11],
|
| 1240 |
+
[12, 13, 14, 15]])
|
| 1241 |
+
|
| 1242 |
+
In [1]: x = np.arange(16, dtype=float).reshape(4,4)
|
| 1243 |
+
|
| 1244 |
+
In [2]: x[0,0] = np.inf
|
| 1245 |
+
|
| 1246 |
+
In [3]: x[0,1] = np.nan
|
| 1247 |
+
|
| 1248 |
+
@doctest float
|
| 1249 |
+
In [4]: x
|
| 1250 |
+
Out[4]:
|
| 1251 |
+
array([[ inf, nan, 2., 3.],
|
| 1252 |
+
[ 4., 5., 6., 7.],
|
| 1253 |
+
[ 8., 9., 10., 11.],
|
| 1254 |
+
[ 12., 13., 14., 15.]])
|
| 1255 |
+
|
| 1256 |
+
|
| 1257 |
+
""",
|
| 1258 |
+
]
|
| 1259 |
+
# skip local-file depending first example:
|
| 1260 |
+
examples = examples[1:]
|
| 1261 |
+
|
| 1262 |
+
#ipython_directive.DEBUG = True # dbg
|
| 1263 |
+
#options = dict(suppress=True) # dbg
|
| 1264 |
+
options = {}
|
| 1265 |
+
for example in examples:
|
| 1266 |
+
content = example.split('\n')
|
| 1267 |
+
IPythonDirective('debug', arguments=None, options=options,
|
| 1268 |
+
content=content, lineno=0,
|
| 1269 |
+
content_offset=None, block_text=None,
|
| 1270 |
+
state=None, state_machine=None,
|
| 1271 |
+
)
|
| 1272 |
+
|
| 1273 |
+
# Run test suite as a script
|
| 1274 |
+
if __name__=='__main__':
|
| 1275 |
+
if not os.path.isdir('_static'):
|
| 1276 |
+
os.mkdir('_static')
|
| 1277 |
+
test()
|
| 1278 |
+
print('All OK? Check figures in _static/')
|
lib/python3.12/site-packages/IPython/testing/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Testing support (tools to test IPython itself).
|
| 2 |
+
"""
|
| 3 |
+
|
| 4 |
+
#-----------------------------------------------------------------------------
|
| 5 |
+
# Copyright (C) 2009-2011 The IPython Development Team
|
| 6 |
+
#
|
| 7 |
+
# Distributed under the terms of the BSD License. The full license is in
|
| 8 |
+
# the file COPYING, distributed as part of this software.
|
| 9 |
+
#-----------------------------------------------------------------------------
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
#-----------------------------------------------------------------------------
|
| 15 |
+
# Constants
|
| 16 |
+
#-----------------------------------------------------------------------------
|
| 17 |
+
|
| 18 |
+
# We scale all timeouts via this factor, slow machines can increase it
|
| 19 |
+
IPYTHON_TESTING_TIMEOUT_SCALE = float(os.getenv(
|
| 20 |
+
'IPYTHON_TESTING_TIMEOUT_SCALE', 1))
|
lib/python3.12/site-packages/IPython/testing/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (420 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/testing/__pycache__/decorators.cpython-312.pyc
ADDED
|
Binary file (4.97 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/testing/__pycache__/globalipapp.cpython-312.pyc
ADDED
|
Binary file (3.87 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/testing/__pycache__/ipunittest.cpython-312.pyc
ADDED
|
Binary file (7.39 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/testing/__pycache__/skipdoctest.cpython-312.pyc
ADDED
|
Binary file (902 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/testing/__pycache__/tools.cpython-312.pyc
ADDED
|
Binary file (17.1 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/testing/decorators.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
import sys
|
| 4 |
+
import tempfile
|
| 5 |
+
from importlib import import_module
|
| 6 |
+
|
| 7 |
+
import pytest
|
| 8 |
+
|
| 9 |
+
# Expose the unittest-driven decorators
|
| 10 |
+
from .ipunittest import ipdoctest, ipdocstring
|
| 11 |
+
|
| 12 |
+
def skipif(skip_condition, msg=None):
|
| 13 |
+
"""Make function raise SkipTest exception if skip_condition is true
|
| 14 |
+
|
| 15 |
+
Parameters
|
| 16 |
+
----------
|
| 17 |
+
|
| 18 |
+
skip_condition : bool or callable
|
| 19 |
+
Flag to determine whether to skip test. If the condition is a
|
| 20 |
+
callable, it is used at runtime to dynamically make the decision. This
|
| 21 |
+
is useful for tests that may require costly imports, to delay the cost
|
| 22 |
+
until the test suite is actually executed.
|
| 23 |
+
msg : string
|
| 24 |
+
Message to give on raising a SkipTest exception.
|
| 25 |
+
|
| 26 |
+
Returns
|
| 27 |
+
-------
|
| 28 |
+
decorator : function
|
| 29 |
+
Decorator, which, when applied to a function, causes SkipTest
|
| 30 |
+
to be raised when the skip_condition was True, and the function
|
| 31 |
+
to be called normally otherwise.
|
| 32 |
+
"""
|
| 33 |
+
if msg is None:
|
| 34 |
+
msg = "Test skipped due to test condition."
|
| 35 |
+
|
| 36 |
+
assert isinstance(skip_condition, bool)
|
| 37 |
+
return pytest.mark.skipif(skip_condition, reason=msg)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# A version with the condition set to true, common case just to attach a message
|
| 41 |
+
# to a skip decorator
|
| 42 |
+
def skip(msg=None):
|
| 43 |
+
"""Decorator factory - mark a test function for skipping from test suite.
|
| 44 |
+
|
| 45 |
+
Parameters
|
| 46 |
+
----------
|
| 47 |
+
msg : string
|
| 48 |
+
Optional message to be added.
|
| 49 |
+
|
| 50 |
+
Returns
|
| 51 |
+
-------
|
| 52 |
+
decorator : function
|
| 53 |
+
Decorator, which, when applied to a function, causes SkipTest
|
| 54 |
+
to be raised, with the optional message added.
|
| 55 |
+
"""
|
| 56 |
+
if msg and not isinstance(msg, str):
|
| 57 |
+
raise ValueError(
|
| 58 |
+
"invalid object passed to `@skip` decorator, did you "
|
| 59 |
+
"meant `@skip()` with brackets ?"
|
| 60 |
+
)
|
| 61 |
+
return skipif(True, msg)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def onlyif(condition, msg):
|
| 65 |
+
"""The reverse from skipif, see skipif for details."""
|
| 66 |
+
|
| 67 |
+
return skipif(not condition, msg)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# -----------------------------------------------------------------------------
|
| 71 |
+
# Utility functions for decorators
|
| 72 |
+
def module_not_available(module):
|
| 73 |
+
"""Can module be imported? Returns true if module does NOT import.
|
| 74 |
+
|
| 75 |
+
This is used to make a decorator to skip tests that require module to be
|
| 76 |
+
available, but delay the 'import numpy' to test execution time.
|
| 77 |
+
"""
|
| 78 |
+
try:
|
| 79 |
+
mod = import_module(module)
|
| 80 |
+
mod_not_avail = False
|
| 81 |
+
except ImportError:
|
| 82 |
+
mod_not_avail = True
|
| 83 |
+
|
| 84 |
+
return mod_not_avail
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# -----------------------------------------------------------------------------
|
| 88 |
+
# Decorators for public use
|
| 89 |
+
|
| 90 |
+
# Decorators to skip certain tests on specific platforms.
|
| 91 |
+
skip_win32 = skipif(sys.platform == "win32", "This test does not run under Windows")
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# Decorators to skip tests if not on specific platforms.
|
| 95 |
+
skip_if_not_win32 = skipif(sys.platform != "win32", "This test only runs under Windows")
|
| 96 |
+
skip_if_not_osx = skipif(
|
| 97 |
+
not sys.platform.startswith("darwin"), "This test only runs under macOS"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
_x11_skip_cond = (
|
| 101 |
+
sys.platform not in ("darwin", "win32") and os.environ.get("DISPLAY", "") == ""
|
| 102 |
+
)
|
| 103 |
+
_x11_skip_msg = "Skipped under *nix when X11/XOrg not available"
|
| 104 |
+
|
| 105 |
+
skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg)
|
| 106 |
+
|
| 107 |
+
# Other skip decorators
|
| 108 |
+
|
| 109 |
+
# generic skip without module
|
| 110 |
+
skip_without = lambda mod: skipif(
|
| 111 |
+
module_not_available(mod), "This test requires %s" % mod
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
skipif_not_numpy = skip_without("numpy")
|
| 115 |
+
|
| 116 |
+
skipif_not_matplotlib = skip_without("matplotlib")
|
| 117 |
+
|
| 118 |
+
# A null 'decorator', useful to make more readable code that needs to pick
|
| 119 |
+
# between different decorators based on OS or other conditions
|
| 120 |
+
null_deco = lambda f: f
|
| 121 |
+
|
| 122 |
+
# Some tests only run where we can use unicode paths. Note that we can't just
|
| 123 |
+
# check os.path.supports_unicode_filenames, which is always False on Linux.
|
| 124 |
+
try:
|
| 125 |
+
f = tempfile.NamedTemporaryFile(prefix="tmp€")
|
| 126 |
+
except UnicodeEncodeError:
|
| 127 |
+
unicode_paths = False
|
| 128 |
+
# TODO: should this be finnally ?
|
| 129 |
+
else:
|
| 130 |
+
unicode_paths = True
|
| 131 |
+
f.close()
|
| 132 |
+
|
| 133 |
+
onlyif_unicode_paths = onlyif(
|
| 134 |
+
unicode_paths,
|
| 135 |
+
("This test is only applicable where we can use unicode in filenames."),
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def onlyif_cmds_exist(*commands):
|
| 140 |
+
"""
|
| 141 |
+
Decorator to skip test when at least one of `commands` is not found.
|
| 142 |
+
"""
|
| 143 |
+
for cmd in commands:
|
| 144 |
+
reason = f"This test runs only if command '{cmd}' is installed"
|
| 145 |
+
if not shutil.which(cmd):
|
| 146 |
+
return pytest.mark.skip(reason=reason)
|
| 147 |
+
return null_deco
|
lib/python3.12/site-packages/IPython/testing/globalipapp.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Global IPython app to support test running.
|
| 2 |
+
|
| 3 |
+
We must start our own ipython object and heavily muck with it so that all the
|
| 4 |
+
modifications IPython makes to system behavior don't send the doctest machinery
|
| 5 |
+
into a fit. This code should be considered a gross hack, but it gets the job
|
| 6 |
+
done.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
# Copyright (c) IPython Development Team.
|
| 10 |
+
# Distributed under the terms of the Modified BSD License.
|
| 11 |
+
|
| 12 |
+
import builtins as builtin_mod
|
| 13 |
+
import sys
|
| 14 |
+
import types
|
| 15 |
+
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
from . import tools
|
| 19 |
+
|
| 20 |
+
from IPython.core import page
|
| 21 |
+
from IPython.utils import io
|
| 22 |
+
from IPython.terminal.interactiveshell import TerminalInteractiveShell
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def get_ipython():
|
| 26 |
+
# This will get replaced by the real thing once we start IPython below
|
| 27 |
+
return start_ipython()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# A couple of methods to override those in the running IPython to interact
|
| 31 |
+
# better with doctest (doctest captures on raw stdout, so we need to direct
|
| 32 |
+
# various types of output there otherwise it will miss them).
|
| 33 |
+
|
| 34 |
+
def xsys(self, cmd):
|
| 35 |
+
"""Replace the default system call with a capturing one for doctest.
|
| 36 |
+
"""
|
| 37 |
+
# We use getoutput, but we need to strip it because pexpect captures
|
| 38 |
+
# the trailing newline differently from commands.getoutput
|
| 39 |
+
print(self.getoutput(cmd, split=False, depth=1).rstrip(), end='', file=sys.stdout)
|
| 40 |
+
sys.stdout.flush()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _showtraceback(self, etype, evalue, stb):
|
| 44 |
+
"""Print the traceback purely on stdout for doctest to capture it.
|
| 45 |
+
"""
|
| 46 |
+
print(self.InteractiveTB.stb2text(stb), file=sys.stdout)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def start_ipython():
|
| 50 |
+
"""Start a global IPython shell, which we need for IPython-specific syntax.
|
| 51 |
+
"""
|
| 52 |
+
global get_ipython
|
| 53 |
+
|
| 54 |
+
# This function should only ever run once!
|
| 55 |
+
if hasattr(start_ipython, 'already_called'):
|
| 56 |
+
return
|
| 57 |
+
start_ipython.already_called = True
|
| 58 |
+
|
| 59 |
+
# Store certain global objects that IPython modifies
|
| 60 |
+
_displayhook = sys.displayhook
|
| 61 |
+
_excepthook = sys.excepthook
|
| 62 |
+
_main = sys.modules.get('__main__')
|
| 63 |
+
|
| 64 |
+
# Create custom argv and namespaces for our IPython to be test-friendly
|
| 65 |
+
config = tools.default_config()
|
| 66 |
+
config.TerminalInteractiveShell.simple_prompt = True
|
| 67 |
+
|
| 68 |
+
# Create and initialize our test-friendly IPython instance.
|
| 69 |
+
shell = TerminalInteractiveShell.instance(config=config,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# A few more tweaks needed for playing nicely with doctests...
|
| 73 |
+
|
| 74 |
+
# remove history file
|
| 75 |
+
shell.tempfiles.append(Path(config.HistoryManager.hist_file))
|
| 76 |
+
|
| 77 |
+
# These traps are normally only active for interactive use, set them
|
| 78 |
+
# permanently since we'll be mocking interactive sessions.
|
| 79 |
+
shell.builtin_trap.activate()
|
| 80 |
+
|
| 81 |
+
# Modify the IPython system call with one that uses getoutput, so that we
|
| 82 |
+
# can capture subcommands and print them to Python's stdout, otherwise the
|
| 83 |
+
# doctest machinery would miss them.
|
| 84 |
+
shell.system = types.MethodType(xsys, shell)
|
| 85 |
+
|
| 86 |
+
shell._showtraceback = types.MethodType(_showtraceback, shell)
|
| 87 |
+
|
| 88 |
+
# IPython is ready, now clean up some global state...
|
| 89 |
+
|
| 90 |
+
# Deactivate the various python system hooks added by ipython for
|
| 91 |
+
# interactive convenience so we don't confuse the doctest system
|
| 92 |
+
sys.modules['__main__'] = _main
|
| 93 |
+
sys.displayhook = _displayhook
|
| 94 |
+
sys.excepthook = _excepthook
|
| 95 |
+
|
| 96 |
+
# So that ipython magics and aliases can be doctested (they work by making
|
| 97 |
+
# a call into a global _ip object). Also make the top-level get_ipython
|
| 98 |
+
# now return this without recursively calling here again.
|
| 99 |
+
_ip = shell
|
| 100 |
+
get_ipython = _ip.get_ipython
|
| 101 |
+
builtin_mod._ip = _ip
|
| 102 |
+
builtin_mod.ip = _ip
|
| 103 |
+
builtin_mod.get_ipython = get_ipython
|
| 104 |
+
|
| 105 |
+
# Override paging, so we don't require user interaction during the tests.
|
| 106 |
+
def nopage(strng, start=0, screen_lines=0, pager_cmd=None):
|
| 107 |
+
if isinstance(strng, dict):
|
| 108 |
+
strng = strng.get('text/plain', '')
|
| 109 |
+
print(strng)
|
| 110 |
+
|
| 111 |
+
page.orig_page = page.pager_page
|
| 112 |
+
page.pager_page = nopage
|
| 113 |
+
|
| 114 |
+
return _ip
|
lib/python3.12/site-packages/IPython/testing/ipunittest.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Experimental code for cleaner support of IPython syntax with unittest.
|
| 2 |
+
|
| 3 |
+
In IPython up until 0.10, we've used very hacked up nose machinery for running
|
| 4 |
+
tests with IPython special syntax, and this has proved to be extremely slow.
|
| 5 |
+
This module provides decorators to try a different approach, stemming from a
|
| 6 |
+
conversation Brian and I (FP) had about this problem Sept/09.
|
| 7 |
+
|
| 8 |
+
The goal is to be able to easily write simple functions that can be seen by
|
| 9 |
+
unittest as tests, and ultimately for these to support doctests with full
|
| 10 |
+
IPython syntax. Nose already offers this based on naming conventions and our
|
| 11 |
+
hackish plugins, but we are seeking to move away from nose dependencies if
|
| 12 |
+
possible.
|
| 13 |
+
|
| 14 |
+
This module follows a different approach, based on decorators.
|
| 15 |
+
|
| 16 |
+
- A decorator called @ipdoctest can mark any function as having a docstring
|
| 17 |
+
that should be viewed as a doctest, but after syntax conversion.
|
| 18 |
+
|
| 19 |
+
Authors
|
| 20 |
+
-------
|
| 21 |
+
|
| 22 |
+
- Fernando Perez <Fernando.Perez@berkeley.edu>
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
#-----------------------------------------------------------------------------
|
| 27 |
+
# Copyright (C) 2009-2011 The IPython Development Team
|
| 28 |
+
#
|
| 29 |
+
# Distributed under the terms of the BSD License. The full license is in
|
| 30 |
+
# the file COPYING, distributed as part of this software.
|
| 31 |
+
#-----------------------------------------------------------------------------
|
| 32 |
+
|
| 33 |
+
#-----------------------------------------------------------------------------
|
| 34 |
+
# Imports
|
| 35 |
+
#-----------------------------------------------------------------------------
|
| 36 |
+
|
| 37 |
+
# Stdlib
|
| 38 |
+
import re
|
| 39 |
+
import sys
|
| 40 |
+
import unittest
|
| 41 |
+
import builtins
|
| 42 |
+
from doctest import DocTestFinder, DocTestRunner, TestResults
|
| 43 |
+
from IPython.terminal.interactiveshell import InteractiveShell
|
| 44 |
+
|
| 45 |
+
#-----------------------------------------------------------------------------
|
| 46 |
+
# Classes and functions
|
| 47 |
+
#-----------------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
def count_failures(runner):
|
| 50 |
+
"""Count number of failures in a doctest runner.
|
| 51 |
+
|
| 52 |
+
Code modeled after the summarize() method in doctest.
|
| 53 |
+
"""
|
| 54 |
+
if sys.version_info < (3, 13):
|
| 55 |
+
return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0]
|
| 56 |
+
else:
|
| 57 |
+
return [
|
| 58 |
+
TestResults(failure, try_)
|
| 59 |
+
for failure, try_, skip in runner._stats.values()
|
| 60 |
+
if failure > 0
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class IPython2PythonConverter:
|
| 65 |
+
"""Convert IPython 'syntax' to valid Python.
|
| 66 |
+
|
| 67 |
+
Eventually this code may grow to be the full IPython syntax conversion
|
| 68 |
+
implementation, but for now it only does prompt conversion."""
|
| 69 |
+
|
| 70 |
+
def __init__(self):
|
| 71 |
+
self.rps1 = re.compile(r'In\ \[\d+\]: ')
|
| 72 |
+
self.rps2 = re.compile(r'\ \ \ \.\.\.+: ')
|
| 73 |
+
self.rout = re.compile(r'Out\[\d+\]: \s*?\n?')
|
| 74 |
+
self.pyps1 = '>>> '
|
| 75 |
+
self.pyps2 = '... '
|
| 76 |
+
self.rpyps1 = re.compile (r'(\s*%s)(.*)$' % self.pyps1)
|
| 77 |
+
self.rpyps2 = re.compile (r'(\s*%s)(.*)$' % self.pyps2)
|
| 78 |
+
|
| 79 |
+
def __call__(self, ds):
|
| 80 |
+
"""Convert IPython prompts to python ones in a string."""
|
| 81 |
+
from . import globalipapp
|
| 82 |
+
|
| 83 |
+
pyps1 = '>>> '
|
| 84 |
+
pyps2 = '... '
|
| 85 |
+
pyout = ''
|
| 86 |
+
|
| 87 |
+
dnew = ds
|
| 88 |
+
dnew = self.rps1.sub(pyps1, dnew)
|
| 89 |
+
dnew = self.rps2.sub(pyps2, dnew)
|
| 90 |
+
dnew = self.rout.sub(pyout, dnew)
|
| 91 |
+
ip = InteractiveShell.instance()
|
| 92 |
+
|
| 93 |
+
# Convert input IPython source into valid Python.
|
| 94 |
+
out = []
|
| 95 |
+
newline = out.append
|
| 96 |
+
for line in dnew.splitlines():
|
| 97 |
+
|
| 98 |
+
mps1 = self.rpyps1.match(line)
|
| 99 |
+
if mps1 is not None:
|
| 100 |
+
prompt, text = mps1.groups()
|
| 101 |
+
newline(prompt+ip.prefilter(text, False))
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
mps2 = self.rpyps2.match(line)
|
| 105 |
+
if mps2 is not None:
|
| 106 |
+
prompt, text = mps2.groups()
|
| 107 |
+
newline(prompt+ip.prefilter(text, True))
|
| 108 |
+
continue
|
| 109 |
+
|
| 110 |
+
newline(line)
|
| 111 |
+
newline('') # ensure a closing newline, needed by doctest
|
| 112 |
+
# print("PYSRC:", '\n'.join(out)) # dbg
|
| 113 |
+
return '\n'.join(out)
|
| 114 |
+
|
| 115 |
+
#return dnew
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class Doc2UnitTester:
|
| 119 |
+
"""Class whose instances act as a decorator for docstring testing.
|
| 120 |
+
|
| 121 |
+
In practice we're only likely to need one instance ever, made below (though
|
| 122 |
+
no attempt is made at turning it into a singleton, there is no need for
|
| 123 |
+
that).
|
| 124 |
+
"""
|
| 125 |
+
def __init__(self, verbose=False):
|
| 126 |
+
"""New decorator.
|
| 127 |
+
|
| 128 |
+
Parameters
|
| 129 |
+
----------
|
| 130 |
+
|
| 131 |
+
verbose : boolean, optional (False)
|
| 132 |
+
Passed to the doctest finder and runner to control verbosity.
|
| 133 |
+
"""
|
| 134 |
+
self.verbose = verbose
|
| 135 |
+
# We can reuse the same finder for all instances
|
| 136 |
+
self.finder = DocTestFinder(verbose=verbose, recurse=False)
|
| 137 |
+
|
| 138 |
+
def __call__(self, func):
|
| 139 |
+
"""Use as a decorator: doctest a function's docstring as a unittest.
|
| 140 |
+
|
| 141 |
+
This version runs normal doctests, but the idea is to make it later run
|
| 142 |
+
ipython syntax instead."""
|
| 143 |
+
|
| 144 |
+
# Capture the enclosing instance with a different name, so the new
|
| 145 |
+
# class below can see it without confusion regarding its own 'self'
|
| 146 |
+
# that will point to the test instance at runtime
|
| 147 |
+
d2u = self
|
| 148 |
+
|
| 149 |
+
# Rewrite the function's docstring to have python syntax
|
| 150 |
+
if func.__doc__ is not None:
|
| 151 |
+
func.__doc__ = ip2py(func.__doc__)
|
| 152 |
+
|
| 153 |
+
# Now, create a tester object that is a real unittest instance, so
|
| 154 |
+
# normal unittest machinery (or Nose, or Trial) can find it.
|
| 155 |
+
class Tester(unittest.TestCase):
|
| 156 |
+
def test(self):
|
| 157 |
+
# Make a new runner per function to be tested
|
| 158 |
+
runner = DocTestRunner(verbose=d2u.verbose)
|
| 159 |
+
for the_test in d2u.finder.find(func, func.__name__):
|
| 160 |
+
runner.run(the_test)
|
| 161 |
+
failed = count_failures(runner)
|
| 162 |
+
if failed:
|
| 163 |
+
# Since we only looked at a single function's docstring,
|
| 164 |
+
# failed should contain at most one item. More than that
|
| 165 |
+
# is a case we can't handle and should error out on
|
| 166 |
+
if len(failed) > 1:
|
| 167 |
+
err = "Invalid number of test results: %s" % failed
|
| 168 |
+
raise ValueError(err)
|
| 169 |
+
# Report a normal failure.
|
| 170 |
+
self.fail('failed doctests: %s' % str(failed[0]))
|
| 171 |
+
|
| 172 |
+
# Rename it so test reports have the original signature.
|
| 173 |
+
Tester.__name__ = func.__name__
|
| 174 |
+
return Tester
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def ipdocstring(func):
|
| 178 |
+
"""Change the function docstring via ip2py.
|
| 179 |
+
"""
|
| 180 |
+
if func.__doc__ is not None:
|
| 181 |
+
func.__doc__ = ip2py(func.__doc__)
|
| 182 |
+
return func
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
# Make an instance of the classes for public use
|
| 186 |
+
ipdoctest = Doc2UnitTester()
|
| 187 |
+
ip2py = IPython2PythonConverter()
|
lib/python3.12/site-packages/IPython/testing/plugin/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/IPython/testing/plugin/dtexample.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Simple example using doctests.
|
| 2 |
+
|
| 3 |
+
This file just contains doctests both using plain python and IPython prompts.
|
| 4 |
+
All tests should be loaded by nose.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def pyfunc():
|
| 11 |
+
"""Some pure python tests...
|
| 12 |
+
|
| 13 |
+
>>> pyfunc()
|
| 14 |
+
'pyfunc'
|
| 15 |
+
|
| 16 |
+
>>> import os
|
| 17 |
+
|
| 18 |
+
>>> 2+3
|
| 19 |
+
5
|
| 20 |
+
|
| 21 |
+
>>> for i in range(3):
|
| 22 |
+
... print(i, end=' ')
|
| 23 |
+
... print(i+1, end=' ')
|
| 24 |
+
...
|
| 25 |
+
0 1 1 2 2 3
|
| 26 |
+
"""
|
| 27 |
+
return 'pyfunc'
|
| 28 |
+
|
| 29 |
+
def ipfunc():
|
| 30 |
+
"""Some ipython tests...
|
| 31 |
+
|
| 32 |
+
In [1]: import os
|
| 33 |
+
|
| 34 |
+
In [3]: 2+3
|
| 35 |
+
Out[3]: 5
|
| 36 |
+
|
| 37 |
+
In [26]: for i in range(3):
|
| 38 |
+
....: print(i, end=' ')
|
| 39 |
+
....: print(i+1, end=' ')
|
| 40 |
+
....:
|
| 41 |
+
0 1 1 2 2 3
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
It's OK to use '_' for the last result, but do NOT try to use IPython's
|
| 45 |
+
numbered history of _NN outputs, since those won't exist under the
|
| 46 |
+
doctest environment:
|
| 47 |
+
|
| 48 |
+
In [7]: 'hi'
|
| 49 |
+
Out[7]: 'hi'
|
| 50 |
+
|
| 51 |
+
In [8]: print(repr(_))
|
| 52 |
+
'hi'
|
| 53 |
+
|
| 54 |
+
In [7]: 3+4
|
| 55 |
+
Out[7]: 7
|
| 56 |
+
|
| 57 |
+
In [8]: _+3
|
| 58 |
+
Out[8]: 10
|
| 59 |
+
|
| 60 |
+
In [9]: ipfunc()
|
| 61 |
+
Out[9]: 'ipfunc'
|
| 62 |
+
"""
|
| 63 |
+
return "ipfunc"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def ipos():
|
| 67 |
+
"""Examples that access the operating system work:
|
| 68 |
+
|
| 69 |
+
In [1]: !echo hello
|
| 70 |
+
hello
|
| 71 |
+
|
| 72 |
+
In [2]: !echo hello > /tmp/foo_iptest
|
| 73 |
+
|
| 74 |
+
In [3]: !cat /tmp/foo_iptest
|
| 75 |
+
hello
|
| 76 |
+
|
| 77 |
+
In [4]: rm -f /tmp/foo_iptest
|
| 78 |
+
"""
|
| 79 |
+
pass
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
ipos.__skip_doctest__ = os.name == "nt"
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def ranfunc():
|
| 86 |
+
"""A function with some random output.
|
| 87 |
+
|
| 88 |
+
Normal examples are verified as usual:
|
| 89 |
+
>>> 1+3
|
| 90 |
+
4
|
| 91 |
+
|
| 92 |
+
But if you put '# random' in the output, it is ignored:
|
| 93 |
+
>>> 1+3
|
| 94 |
+
junk goes here... # random
|
| 95 |
+
|
| 96 |
+
>>> 1+2
|
| 97 |
+
again, anything goes #random
|
| 98 |
+
if multiline, the random mark is only needed once.
|
| 99 |
+
|
| 100 |
+
>>> 1+2
|
| 101 |
+
You can also put the random marker at the end:
|
| 102 |
+
# random
|
| 103 |
+
|
| 104 |
+
>>> 1+2
|
| 105 |
+
# random
|
| 106 |
+
.. or at the beginning.
|
| 107 |
+
|
| 108 |
+
More correct input is properly verified:
|
| 109 |
+
>>> ranfunc()
|
| 110 |
+
'ranfunc'
|
| 111 |
+
"""
|
| 112 |
+
return 'ranfunc'
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def random_all():
|
| 116 |
+
"""A function where we ignore the output of ALL examples.
|
| 117 |
+
|
| 118 |
+
Examples:
|
| 119 |
+
|
| 120 |
+
# all-random
|
| 121 |
+
|
| 122 |
+
This mark tells the testing machinery that all subsequent examples should
|
| 123 |
+
be treated as random (ignoring their output). They are still executed,
|
| 124 |
+
so if a they raise an error, it will be detected as such, but their
|
| 125 |
+
output is completely ignored.
|
| 126 |
+
|
| 127 |
+
>>> 1+3
|
| 128 |
+
junk goes here...
|
| 129 |
+
|
| 130 |
+
>>> 1+3
|
| 131 |
+
klasdfj;
|
| 132 |
+
|
| 133 |
+
>>> 1+2
|
| 134 |
+
again, anything goes
|
| 135 |
+
blah...
|
| 136 |
+
"""
|
| 137 |
+
pass
|
| 138 |
+
|
| 139 |
+
def iprand():
|
| 140 |
+
"""Some ipython tests with random output.
|
| 141 |
+
|
| 142 |
+
In [7]: 3+4
|
| 143 |
+
Out[7]: 7
|
| 144 |
+
|
| 145 |
+
In [8]: print('hello')
|
| 146 |
+
world # random
|
| 147 |
+
|
| 148 |
+
In [9]: iprand()
|
| 149 |
+
Out[9]: 'iprand'
|
| 150 |
+
"""
|
| 151 |
+
return 'iprand'
|
| 152 |
+
|
| 153 |
+
def iprand_all():
|
| 154 |
+
"""Some ipython tests with fully random output.
|
| 155 |
+
|
| 156 |
+
# all-random
|
| 157 |
+
|
| 158 |
+
In [7]: 1
|
| 159 |
+
Out[7]: 99
|
| 160 |
+
|
| 161 |
+
In [8]: print('hello')
|
| 162 |
+
world
|
| 163 |
+
|
| 164 |
+
In [9]: iprand_all()
|
| 165 |
+
Out[9]: 'junk'
|
| 166 |
+
"""
|
| 167 |
+
return 'iprand_all'
|
lib/python3.12/site-packages/IPython/testing/plugin/ipdoctest.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Nose Plugin that supports IPython doctests.
|
| 2 |
+
|
| 3 |
+
Limitations:
|
| 4 |
+
|
| 5 |
+
- When generating examples for use as doctests, make sure that you have
|
| 6 |
+
pretty-printing OFF. This can be done either by setting the
|
| 7 |
+
``PlainTextFormatter.pprint`` option in your configuration file to False, or
|
| 8 |
+
by interactively disabling it with %Pprint. This is required so that IPython
|
| 9 |
+
output matches that of normal Python, which is used by doctest for internal
|
| 10 |
+
execution.
|
| 11 |
+
|
| 12 |
+
- Do not rely on specific prompt numbers for results (such as using
|
| 13 |
+
'_34==True', for example). For IPython tests run via an external process the
|
| 14 |
+
prompt numbers may be different, and IPython tests run as normal python code
|
| 15 |
+
won't even have these special _NN variables set at all.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
#-----------------------------------------------------------------------------
|
| 19 |
+
# Module imports
|
| 20 |
+
|
| 21 |
+
# From the standard library
|
| 22 |
+
import doctest
|
| 23 |
+
import logging
|
| 24 |
+
import re
|
| 25 |
+
|
| 26 |
+
from testpath import modified_env
|
| 27 |
+
|
| 28 |
+
#-----------------------------------------------------------------------------
|
| 29 |
+
# Module globals and other constants
|
| 30 |
+
#-----------------------------------------------------------------------------
|
| 31 |
+
|
| 32 |
+
log = logging.getLogger(__name__)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
#-----------------------------------------------------------------------------
|
| 36 |
+
# Classes and functions
|
| 37 |
+
#-----------------------------------------------------------------------------
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class DocTestFinder(doctest.DocTestFinder):
|
| 41 |
+
def _get_test(self, obj, name, module, globs, source_lines):
|
| 42 |
+
test = super()._get_test(obj, name, module, globs, source_lines)
|
| 43 |
+
|
| 44 |
+
if bool(getattr(obj, "__skip_doctest__", False)) and test is not None:
|
| 45 |
+
for example in test.examples:
|
| 46 |
+
example.options[doctest.SKIP] = True
|
| 47 |
+
|
| 48 |
+
return test
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class IPDoctestOutputChecker(doctest.OutputChecker):
|
| 52 |
+
"""Second-chance checker with support for random tests.
|
| 53 |
+
|
| 54 |
+
If the default comparison doesn't pass, this checker looks in the expected
|
| 55 |
+
output string for flags that tell us to ignore the output.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
random_re = re.compile(r'#\s*random\s+')
|
| 59 |
+
|
| 60 |
+
def check_output(self, want, got, optionflags):
|
| 61 |
+
"""Check output, accepting special markers embedded in the output.
|
| 62 |
+
|
| 63 |
+
If the output didn't pass the default validation but the special string
|
| 64 |
+
'#random' is included, we accept it."""
|
| 65 |
+
|
| 66 |
+
# Let the original tester verify first, in case people have valid tests
|
| 67 |
+
# that happen to have a comment saying '#random' embedded in.
|
| 68 |
+
ret = doctest.OutputChecker.check_output(self, want, got,
|
| 69 |
+
optionflags)
|
| 70 |
+
if not ret and self.random_re.search(want):
|
| 71 |
+
# print('RANDOM OK:',want, file=sys.stderr) # dbg
|
| 72 |
+
return True
|
| 73 |
+
|
| 74 |
+
return ret
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# A simple subclassing of the original with a different class name, so we can
|
| 78 |
+
# distinguish and treat differently IPython examples from pure python ones.
|
| 79 |
+
class IPExample(doctest.Example): pass
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class IPDocTestParser(doctest.DocTestParser):
|
| 83 |
+
"""
|
| 84 |
+
A class used to parse strings containing doctest examples.
|
| 85 |
+
|
| 86 |
+
Note: This is a version modified to properly recognize IPython input and
|
| 87 |
+
convert any IPython examples into valid Python ones.
|
| 88 |
+
"""
|
| 89 |
+
# This regular expression is used to find doctest examples in a
|
| 90 |
+
# string. It defines three groups: `source` is the source code
|
| 91 |
+
# (including leading indentation and prompts); `indent` is the
|
| 92 |
+
# indentation of the first (PS1) line of the source code; and
|
| 93 |
+
# `want` is the expected output (including leading indentation).
|
| 94 |
+
|
| 95 |
+
# Classic Python prompts or default IPython ones
|
| 96 |
+
_PS1_PY = r'>>>'
|
| 97 |
+
_PS2_PY = r'\.\.\.'
|
| 98 |
+
|
| 99 |
+
_PS1_IP = r'In\ \[\d+\]:'
|
| 100 |
+
_PS2_IP = r'\ \ \ \.\.\.+:'
|
| 101 |
+
|
| 102 |
+
_RE_TPL = r'''
|
| 103 |
+
# Source consists of a PS1 line followed by zero or more PS2 lines.
|
| 104 |
+
(?P<source>
|
| 105 |
+
(?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
|
| 106 |
+
(?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
|
| 107 |
+
\n? # a newline
|
| 108 |
+
# Want consists of any non-blank lines that do not start with PS1.
|
| 109 |
+
(?P<want> (?:(?![ ]*$) # Not a blank line
|
| 110 |
+
(?![ ]*%s) # Not a line starting with PS1
|
| 111 |
+
(?![ ]*%s) # Not a line starting with PS2
|
| 112 |
+
.*$\n? # But any other line
|
| 113 |
+
)*)
|
| 114 |
+
'''
|
| 115 |
+
|
| 116 |
+
_EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
|
| 117 |
+
re.MULTILINE | re.VERBOSE)
|
| 118 |
+
|
| 119 |
+
_EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
|
| 120 |
+
re.MULTILINE | re.VERBOSE)
|
| 121 |
+
|
| 122 |
+
# Mark a test as being fully random. In this case, we simply append the
|
| 123 |
+
# random marker ('#random') to each individual example's output. This way
|
| 124 |
+
# we don't need to modify any other code.
|
| 125 |
+
_RANDOM_TEST = re.compile(r'#\s*all-random\s+')
|
| 126 |
+
|
| 127 |
+
def ip2py(self,source):
|
| 128 |
+
"""Convert input IPython source into valid Python."""
|
| 129 |
+
block = _ip.input_transformer_manager.transform_cell(source)
|
| 130 |
+
if len(block.splitlines()) == 1:
|
| 131 |
+
return _ip.prefilter(block)
|
| 132 |
+
else:
|
| 133 |
+
return block
|
| 134 |
+
|
| 135 |
+
def parse(self, string, name='<string>'):
|
| 136 |
+
"""
|
| 137 |
+
Divide the given string into examples and intervening text,
|
| 138 |
+
and return them as a list of alternating Examples and strings.
|
| 139 |
+
Line numbers for the Examples are 0-based. The optional
|
| 140 |
+
argument `name` is a name identifying this string, and is only
|
| 141 |
+
used for error messages.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
# print('Parse string:\n',string) # dbg
|
| 145 |
+
|
| 146 |
+
string = string.expandtabs()
|
| 147 |
+
# If all lines begin with the same indentation, then strip it.
|
| 148 |
+
min_indent = self._min_indent(string)
|
| 149 |
+
if min_indent > 0:
|
| 150 |
+
string = '\n'.join([l[min_indent:] for l in string.split('\n')])
|
| 151 |
+
|
| 152 |
+
output = []
|
| 153 |
+
charno, lineno = 0, 0
|
| 154 |
+
|
| 155 |
+
# We make 'all random' tests by adding the '# random' mark to every
|
| 156 |
+
# block of output in the test.
|
| 157 |
+
if self._RANDOM_TEST.search(string):
|
| 158 |
+
random_marker = '\n# random'
|
| 159 |
+
else:
|
| 160 |
+
random_marker = ''
|
| 161 |
+
|
| 162 |
+
# Whether to convert the input from ipython to python syntax
|
| 163 |
+
ip2py = False
|
| 164 |
+
# Find all doctest examples in the string. First, try them as Python
|
| 165 |
+
# examples, then as IPython ones
|
| 166 |
+
terms = list(self._EXAMPLE_RE_PY.finditer(string))
|
| 167 |
+
if terms:
|
| 168 |
+
# Normal Python example
|
| 169 |
+
Example = doctest.Example
|
| 170 |
+
else:
|
| 171 |
+
# It's an ipython example.
|
| 172 |
+
terms = list(self._EXAMPLE_RE_IP.finditer(string))
|
| 173 |
+
Example = IPExample
|
| 174 |
+
ip2py = True
|
| 175 |
+
|
| 176 |
+
for m in terms:
|
| 177 |
+
# Add the pre-example text to `output`.
|
| 178 |
+
output.append(string[charno:m.start()])
|
| 179 |
+
# Update lineno (lines before this example)
|
| 180 |
+
lineno += string.count('\n', charno, m.start())
|
| 181 |
+
# Extract info from the regexp match.
|
| 182 |
+
(source, options, want, exc_msg) = \
|
| 183 |
+
self._parse_example(m, name, lineno,ip2py)
|
| 184 |
+
|
| 185 |
+
# Append the random-output marker (it defaults to empty in most
|
| 186 |
+
# cases, it's only non-empty for 'all-random' tests):
|
| 187 |
+
want += random_marker
|
| 188 |
+
|
| 189 |
+
# Create an Example, and add it to the list.
|
| 190 |
+
if not self._IS_BLANK_OR_COMMENT(source):
|
| 191 |
+
output.append(Example(source, want, exc_msg,
|
| 192 |
+
lineno=lineno,
|
| 193 |
+
indent=min_indent+len(m.group('indent')),
|
| 194 |
+
options=options))
|
| 195 |
+
# Update lineno (lines inside this example)
|
| 196 |
+
lineno += string.count('\n', m.start(), m.end())
|
| 197 |
+
# Update charno.
|
| 198 |
+
charno = m.end()
|
| 199 |
+
# Add any remaining post-example text to `output`.
|
| 200 |
+
output.append(string[charno:])
|
| 201 |
+
return output
|
| 202 |
+
|
| 203 |
+
def _parse_example(self, m, name, lineno,ip2py=False):
|
| 204 |
+
"""
|
| 205 |
+
Given a regular expression match from `_EXAMPLE_RE` (`m`),
|
| 206 |
+
return a pair `(source, want)`, where `source` is the matched
|
| 207 |
+
example's source code (with prompts and indentation stripped);
|
| 208 |
+
and `want` is the example's expected output (with indentation
|
| 209 |
+
stripped).
|
| 210 |
+
|
| 211 |
+
`name` is the string's name, and `lineno` is the line number
|
| 212 |
+
where the example starts; both are used for error messages.
|
| 213 |
+
|
| 214 |
+
Optional:
|
| 215 |
+
`ip2py`: if true, filter the input via IPython to convert the syntax
|
| 216 |
+
into valid python.
|
| 217 |
+
"""
|
| 218 |
+
|
| 219 |
+
# Get the example's indentation level.
|
| 220 |
+
indent = len(m.group('indent'))
|
| 221 |
+
|
| 222 |
+
# Divide source into lines; check that they're properly
|
| 223 |
+
# indented; and then strip their indentation & prompts.
|
| 224 |
+
source_lines = m.group('source').split('\n')
|
| 225 |
+
|
| 226 |
+
# We're using variable-length input prompts
|
| 227 |
+
ps1 = m.group('ps1')
|
| 228 |
+
ps2 = m.group('ps2')
|
| 229 |
+
ps1_len = len(ps1)
|
| 230 |
+
|
| 231 |
+
self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
|
| 232 |
+
if ps2:
|
| 233 |
+
self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
|
| 234 |
+
|
| 235 |
+
source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
|
| 236 |
+
|
| 237 |
+
if ip2py:
|
| 238 |
+
# Convert source input from IPython into valid Python syntax
|
| 239 |
+
source = self.ip2py(source)
|
| 240 |
+
|
| 241 |
+
# Divide want into lines; check that it's properly indented; and
|
| 242 |
+
# then strip the indentation. Spaces before the last newline should
|
| 243 |
+
# be preserved, so plain rstrip() isn't good enough.
|
| 244 |
+
want = m.group('want')
|
| 245 |
+
want_lines = want.split('\n')
|
| 246 |
+
if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
|
| 247 |
+
del want_lines[-1] # forget final newline & spaces after it
|
| 248 |
+
self._check_prefix(want_lines, ' '*indent, name,
|
| 249 |
+
lineno + len(source_lines))
|
| 250 |
+
|
| 251 |
+
# Remove ipython output prompt that might be present in the first line
|
| 252 |
+
want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
|
| 253 |
+
|
| 254 |
+
want = '\n'.join([wl[indent:] for wl in want_lines])
|
| 255 |
+
|
| 256 |
+
# If `want` contains a traceback message, then extract it.
|
| 257 |
+
m = self._EXCEPTION_RE.match(want)
|
| 258 |
+
if m:
|
| 259 |
+
exc_msg = m.group('msg')
|
| 260 |
+
else:
|
| 261 |
+
exc_msg = None
|
| 262 |
+
|
| 263 |
+
# Extract options from the source.
|
| 264 |
+
options = self._find_options(source, name, lineno)
|
| 265 |
+
|
| 266 |
+
return source, options, want, exc_msg
|
| 267 |
+
|
| 268 |
+
def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
|
| 269 |
+
"""
|
| 270 |
+
Given the lines of a source string (including prompts and
|
| 271 |
+
leading indentation), check to make sure that every prompt is
|
| 272 |
+
followed by a space character. If any line is not followed by
|
| 273 |
+
a space character, then raise ValueError.
|
| 274 |
+
|
| 275 |
+
Note: IPython-modified version which takes the input prompt length as a
|
| 276 |
+
parameter, so that prompts of variable length can be dealt with.
|
| 277 |
+
"""
|
| 278 |
+
space_idx = indent+ps1_len
|
| 279 |
+
min_len = space_idx+1
|
| 280 |
+
for i, line in enumerate(lines):
|
| 281 |
+
if len(line) >= min_len and line[space_idx] != ' ':
|
| 282 |
+
raise ValueError('line %r of the docstring for %s '
|
| 283 |
+
'lacks blank after %s: %r' %
|
| 284 |
+
(lineno+i+1, name,
|
| 285 |
+
line[indent:space_idx], line))
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
SKIP = doctest.register_optionflag('SKIP')
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
class IPDocTestRunner(doctest.DocTestRunner):
|
| 292 |
+
"""Test runner that synchronizes the IPython namespace with test globals.
|
| 293 |
+
"""
|
| 294 |
+
|
| 295 |
+
def run(self, test, compileflags=None, out=None, clear_globs=True):
|
| 296 |
+
# Override terminal size to standardise traceback format
|
| 297 |
+
with modified_env({'COLUMNS': '80', 'LINES': '24'}):
|
| 298 |
+
return super(IPDocTestRunner,self).run(test,
|
| 299 |
+
compileflags,out,clear_globs)
|
lib/python3.12/site-packages/IPython/testing/plugin/pytest_ipdoctest.py
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Based on Pytest doctest.py
|
| 2 |
+
# Original license:
|
| 3 |
+
# The MIT License (MIT)
|
| 4 |
+
#
|
| 5 |
+
# Copyright (c) 2004-2021 Holger Krekel and others
|
| 6 |
+
"""Discover and run ipdoctests in modules and test files."""
|
| 7 |
+
|
| 8 |
+
import bdb
|
| 9 |
+
import builtins
|
| 10 |
+
import inspect
|
| 11 |
+
import os
|
| 12 |
+
import platform
|
| 13 |
+
import sys
|
| 14 |
+
import traceback
|
| 15 |
+
import types
|
| 16 |
+
import warnings
|
| 17 |
+
from contextlib import contextmanager
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import (
|
| 20 |
+
TYPE_CHECKING,
|
| 21 |
+
Any,
|
| 22 |
+
Dict,
|
| 23 |
+
List,
|
| 24 |
+
Optional,
|
| 25 |
+
Tuple,
|
| 26 |
+
Type,
|
| 27 |
+
Union,
|
| 28 |
+
)
|
| 29 |
+
from re import Pattern
|
| 30 |
+
from collections.abc import Callable, Generator, Iterable, Sequence
|
| 31 |
+
|
| 32 |
+
import pytest
|
| 33 |
+
from _pytest import outcomes
|
| 34 |
+
from _pytest._code.code import ExceptionInfo, ReprFileLocation, TerminalRepr
|
| 35 |
+
from _pytest._io import TerminalWriter
|
| 36 |
+
from _pytest.compat import safe_getattr
|
| 37 |
+
from _pytest.config import Config
|
| 38 |
+
from _pytest.config.argparsing import Parser
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
from _pytest.fixtures import TopRequest as FixtureRequest
|
| 42 |
+
except ImportError:
|
| 43 |
+
from _pytest.fixtures import FixtureRequest
|
| 44 |
+
from _pytest.nodes import Collector
|
| 45 |
+
from _pytest.outcomes import OutcomeException
|
| 46 |
+
from _pytest.pathlib import fnmatch_ex, import_path
|
| 47 |
+
from _pytest.python_api import approx
|
| 48 |
+
from _pytest.warning_types import PytestWarning
|
| 49 |
+
|
| 50 |
+
if TYPE_CHECKING:
|
| 51 |
+
import doctest
|
| 52 |
+
|
| 53 |
+
from .ipdoctest import IPDoctestOutputChecker
|
| 54 |
+
|
| 55 |
+
DOCTEST_REPORT_CHOICE_NONE = "none"
|
| 56 |
+
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
| 57 |
+
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
|
| 58 |
+
DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
|
| 59 |
+
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
|
| 60 |
+
|
| 61 |
+
DOCTEST_REPORT_CHOICES = (
|
| 62 |
+
DOCTEST_REPORT_CHOICE_NONE,
|
| 63 |
+
DOCTEST_REPORT_CHOICE_CDIFF,
|
| 64 |
+
DOCTEST_REPORT_CHOICE_NDIFF,
|
| 65 |
+
DOCTEST_REPORT_CHOICE_UDIFF,
|
| 66 |
+
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Lazy definition of runner class
|
| 70 |
+
RUNNER_CLASS = None
|
| 71 |
+
# Lazy definition of output checker class
|
| 72 |
+
CHECKER_CLASS: Optional[Type["IPDoctestOutputChecker"]] = None
|
| 73 |
+
|
| 74 |
+
pytest_version = tuple([int(part) for part in pytest.__version__.split(".")])
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def pytest_addoption(parser: Parser) -> None:
|
| 78 |
+
parser.addini(
|
| 79 |
+
"ipdoctest_optionflags",
|
| 80 |
+
"option flags for ipdoctests",
|
| 81 |
+
type="args",
|
| 82 |
+
default=["ELLIPSIS"],
|
| 83 |
+
)
|
| 84 |
+
parser.addini(
|
| 85 |
+
"ipdoctest_encoding", "encoding used for ipdoctest files", default="utf-8"
|
| 86 |
+
)
|
| 87 |
+
group = parser.getgroup("collect")
|
| 88 |
+
group.addoption(
|
| 89 |
+
"--ipdoctest-modules",
|
| 90 |
+
action="store_true",
|
| 91 |
+
default=False,
|
| 92 |
+
help="run ipdoctests in all .py modules",
|
| 93 |
+
dest="ipdoctestmodules",
|
| 94 |
+
)
|
| 95 |
+
group.addoption(
|
| 96 |
+
"--ipdoctest-report",
|
| 97 |
+
type=str.lower,
|
| 98 |
+
default="udiff",
|
| 99 |
+
help="choose another output format for diffs on ipdoctest failure",
|
| 100 |
+
choices=DOCTEST_REPORT_CHOICES,
|
| 101 |
+
dest="ipdoctestreport",
|
| 102 |
+
)
|
| 103 |
+
group.addoption(
|
| 104 |
+
"--ipdoctest-glob",
|
| 105 |
+
action="append",
|
| 106 |
+
default=[],
|
| 107 |
+
metavar="pat",
|
| 108 |
+
help="ipdoctests file matching pattern, default: test*.txt",
|
| 109 |
+
dest="ipdoctestglob",
|
| 110 |
+
)
|
| 111 |
+
group.addoption(
|
| 112 |
+
"--ipdoctest-ignore-import-errors",
|
| 113 |
+
action="store_true",
|
| 114 |
+
default=False,
|
| 115 |
+
help="ignore ipdoctest ImportErrors",
|
| 116 |
+
dest="ipdoctest_ignore_import_errors",
|
| 117 |
+
)
|
| 118 |
+
group.addoption(
|
| 119 |
+
"--ipdoctest-continue-on-failure",
|
| 120 |
+
action="store_true",
|
| 121 |
+
default=False,
|
| 122 |
+
help="for a given ipdoctest, continue to run after the first failure",
|
| 123 |
+
dest="ipdoctest_continue_on_failure",
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def pytest_unconfigure() -> None:
|
| 128 |
+
global RUNNER_CLASS
|
| 129 |
+
|
| 130 |
+
RUNNER_CLASS = None
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def pytest_collect_file(
|
| 134 |
+
file_path: Path,
|
| 135 |
+
parent: Collector,
|
| 136 |
+
) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
|
| 137 |
+
config = parent.config
|
| 138 |
+
if file_path.suffix == ".py":
|
| 139 |
+
if config.option.ipdoctestmodules and not any(
|
| 140 |
+
(_is_setup_py(file_path), _is_main_py(file_path))
|
| 141 |
+
):
|
| 142 |
+
mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path)
|
| 143 |
+
return mod
|
| 144 |
+
elif _is_ipdoctest(config, file_path, parent):
|
| 145 |
+
txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path)
|
| 146 |
+
return txt
|
| 147 |
+
return None
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
if pytest_version[0] < 7:
|
| 151 |
+
_collect_file = pytest_collect_file
|
| 152 |
+
|
| 153 |
+
def pytest_collect_file(
|
| 154 |
+
path,
|
| 155 |
+
parent: Collector,
|
| 156 |
+
) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
|
| 157 |
+
return _collect_file(Path(path), parent)
|
| 158 |
+
|
| 159 |
+
_import_path = import_path
|
| 160 |
+
|
| 161 |
+
def import_path(path, root):
|
| 162 |
+
import py.path
|
| 163 |
+
|
| 164 |
+
return _import_path(py.path.local(path))
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def _is_setup_py(path: Path) -> bool:
|
| 168 |
+
if path.name != "setup.py":
|
| 169 |
+
return False
|
| 170 |
+
contents = path.read_bytes()
|
| 171 |
+
return b"setuptools" in contents or b"distutils" in contents
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool:
|
| 175 |
+
if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
|
| 176 |
+
return True
|
| 177 |
+
globs = config.getoption("ipdoctestglob") or ["test*.txt"]
|
| 178 |
+
return any(fnmatch_ex(glob, path) for glob in globs)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _is_main_py(path: Path) -> bool:
|
| 182 |
+
return path.name == "__main__.py"
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
class ReprFailDoctest(TerminalRepr):
|
| 186 |
+
def __init__(
|
| 187 |
+
self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
|
| 188 |
+
) -> None:
|
| 189 |
+
self.reprlocation_lines = reprlocation_lines
|
| 190 |
+
|
| 191 |
+
def toterminal(self, tw: TerminalWriter) -> None:
|
| 192 |
+
for reprlocation, lines in self.reprlocation_lines:
|
| 193 |
+
for line in lines:
|
| 194 |
+
tw.line(line)
|
| 195 |
+
reprlocation.toterminal(tw)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class MultipleDoctestFailures(Exception):
|
| 199 |
+
def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
|
| 200 |
+
super().__init__()
|
| 201 |
+
self.failures = failures
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _init_runner_class() -> Type["IPDocTestRunner"]:
|
| 205 |
+
import doctest
|
| 206 |
+
from .ipdoctest import IPDocTestRunner
|
| 207 |
+
|
| 208 |
+
class PytestDoctestRunner(IPDocTestRunner):
|
| 209 |
+
"""Runner to collect failures.
|
| 210 |
+
|
| 211 |
+
Note that the out variable in this case is a list instead of a
|
| 212 |
+
stdout-like object.
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
def __init__(
|
| 216 |
+
self,
|
| 217 |
+
checker: Optional["IPDoctestOutputChecker"] = None,
|
| 218 |
+
verbose: Optional[bool] = None,
|
| 219 |
+
optionflags: int = 0,
|
| 220 |
+
continue_on_failure: bool = True,
|
| 221 |
+
) -> None:
|
| 222 |
+
super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
|
| 223 |
+
self.continue_on_failure = continue_on_failure
|
| 224 |
+
|
| 225 |
+
def report_failure(
|
| 226 |
+
self,
|
| 227 |
+
out,
|
| 228 |
+
test: "doctest.DocTest",
|
| 229 |
+
example: "doctest.Example",
|
| 230 |
+
got: str,
|
| 231 |
+
) -> None:
|
| 232 |
+
failure = doctest.DocTestFailure(test, example, got)
|
| 233 |
+
if self.continue_on_failure:
|
| 234 |
+
out.append(failure)
|
| 235 |
+
else:
|
| 236 |
+
raise failure
|
| 237 |
+
|
| 238 |
+
def report_unexpected_exception(
|
| 239 |
+
self,
|
| 240 |
+
out,
|
| 241 |
+
test: "doctest.DocTest",
|
| 242 |
+
example: "doctest.Example",
|
| 243 |
+
exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
|
| 244 |
+
) -> None:
|
| 245 |
+
if isinstance(exc_info[1], OutcomeException):
|
| 246 |
+
raise exc_info[1]
|
| 247 |
+
if isinstance(exc_info[1], bdb.BdbQuit):
|
| 248 |
+
outcomes.exit("Quitting debugger")
|
| 249 |
+
failure = doctest.UnexpectedException(test, example, exc_info)
|
| 250 |
+
if self.continue_on_failure:
|
| 251 |
+
out.append(failure)
|
| 252 |
+
else:
|
| 253 |
+
raise failure
|
| 254 |
+
|
| 255 |
+
return PytestDoctestRunner
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _get_runner(
|
| 259 |
+
checker: Optional["IPDoctestOutputChecker"] = None,
|
| 260 |
+
verbose: Optional[bool] = None,
|
| 261 |
+
optionflags: int = 0,
|
| 262 |
+
continue_on_failure: bool = True,
|
| 263 |
+
) -> "IPDocTestRunner":
|
| 264 |
+
# We need this in order to do a lazy import on doctest
|
| 265 |
+
global RUNNER_CLASS
|
| 266 |
+
if RUNNER_CLASS is None:
|
| 267 |
+
RUNNER_CLASS = _init_runner_class()
|
| 268 |
+
# Type ignored because the continue_on_failure argument is only defined on
|
| 269 |
+
# PytestDoctestRunner, which is lazily defined so can't be used as a type.
|
| 270 |
+
return RUNNER_CLASS( # type: ignore
|
| 271 |
+
checker=checker,
|
| 272 |
+
verbose=verbose,
|
| 273 |
+
optionflags=optionflags,
|
| 274 |
+
continue_on_failure=continue_on_failure,
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
class IPDoctestItem(pytest.Item):
|
| 279 |
+
_user_ns_orig: Dict[str, Any]
|
| 280 |
+
|
| 281 |
+
def __init__(
|
| 282 |
+
self,
|
| 283 |
+
name: str,
|
| 284 |
+
parent: "Union[IPDoctestTextfile, IPDoctestModule]",
|
| 285 |
+
runner: Optional["IPDocTestRunner"] = None,
|
| 286 |
+
dtest: Optional["doctest.DocTest"] = None,
|
| 287 |
+
) -> None:
|
| 288 |
+
super().__init__(name, parent)
|
| 289 |
+
self.runner = runner
|
| 290 |
+
self.dtest = dtest
|
| 291 |
+
self.obj = None
|
| 292 |
+
self.fixture_request: Optional[FixtureRequest] = None
|
| 293 |
+
self._user_ns_orig = {}
|
| 294 |
+
|
| 295 |
+
@classmethod
|
| 296 |
+
def from_parent( # type: ignore
|
| 297 |
+
cls,
|
| 298 |
+
parent: "Union[IPDoctestTextfile, IPDoctestModule]",
|
| 299 |
+
*,
|
| 300 |
+
name: str,
|
| 301 |
+
runner: "IPDocTestRunner",
|
| 302 |
+
dtest: "doctest.DocTest",
|
| 303 |
+
):
|
| 304 |
+
# incompatible signature due to imposed limits on subclass
|
| 305 |
+
"""The public named constructor."""
|
| 306 |
+
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
| 307 |
+
|
| 308 |
+
def setup(self) -> None:
|
| 309 |
+
if self.dtest is not None:
|
| 310 |
+
self.fixture_request = _setup_fixtures(self)
|
| 311 |
+
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
| 312 |
+
for name, value in self.fixture_request.getfixturevalue(
|
| 313 |
+
"ipdoctest_namespace"
|
| 314 |
+
).items():
|
| 315 |
+
globs[name] = value
|
| 316 |
+
self.dtest.globs.update(globs)
|
| 317 |
+
|
| 318 |
+
from .ipdoctest import IPExample
|
| 319 |
+
|
| 320 |
+
if isinstance(self.dtest.examples[0], IPExample):
|
| 321 |
+
# for IPython examples *only*, we swap the globals with the ipython
|
| 322 |
+
# namespace, after updating it with the globals (which doctest
|
| 323 |
+
# fills with the necessary info from the module being tested).
|
| 324 |
+
self._user_ns_orig = {}
|
| 325 |
+
self._user_ns_orig.update(_ip.user_ns)
|
| 326 |
+
_ip.user_ns.update(self.dtest.globs)
|
| 327 |
+
# We must remove the _ key in the namespace, so that Python's
|
| 328 |
+
# doctest code sets it naturally
|
| 329 |
+
_ip.user_ns.pop("_", None)
|
| 330 |
+
_ip.user_ns["__builtins__"] = builtins
|
| 331 |
+
self.dtest.globs = _ip.user_ns
|
| 332 |
+
|
| 333 |
+
def teardown(self) -> None:
|
| 334 |
+
from .ipdoctest import IPExample
|
| 335 |
+
|
| 336 |
+
# Undo the test.globs reassignment we made
|
| 337 |
+
if isinstance(self.dtest.examples[0], IPExample):
|
| 338 |
+
self.dtest.globs = {}
|
| 339 |
+
_ip.user_ns.clear()
|
| 340 |
+
_ip.user_ns.update(self._user_ns_orig)
|
| 341 |
+
del self._user_ns_orig
|
| 342 |
+
|
| 343 |
+
self.dtest.globs.clear()
|
| 344 |
+
|
| 345 |
+
def runtest(self) -> None:
|
| 346 |
+
assert self.dtest is not None
|
| 347 |
+
assert self.runner is not None
|
| 348 |
+
_check_all_skipped(self.dtest)
|
| 349 |
+
self._disable_output_capturing_for_darwin()
|
| 350 |
+
failures: List[doctest.DocTestFailure] = []
|
| 351 |
+
|
| 352 |
+
# exec(compile(..., "single", ...), ...) puts result in builtins._
|
| 353 |
+
had_underscore_value = hasattr(builtins, "_")
|
| 354 |
+
underscore_original_value = getattr(builtins, "_", None)
|
| 355 |
+
|
| 356 |
+
# Save our current directory and switch out to the one where the
|
| 357 |
+
# test was originally created, in case another doctest did a
|
| 358 |
+
# directory change. We'll restore this in the finally clause.
|
| 359 |
+
curdir = os.getcwd()
|
| 360 |
+
os.chdir(self.fspath.dirname)
|
| 361 |
+
try:
|
| 362 |
+
# Type ignored because we change the type of `out` from what
|
| 363 |
+
# ipdoctest expects.
|
| 364 |
+
self.runner.run(self.dtest, out=failures, clear_globs=False) # type: ignore[arg-type]
|
| 365 |
+
finally:
|
| 366 |
+
os.chdir(curdir)
|
| 367 |
+
if had_underscore_value:
|
| 368 |
+
setattr(builtins, "_", underscore_original_value)
|
| 369 |
+
elif hasattr(builtins, "_"):
|
| 370 |
+
delattr(builtins, "_")
|
| 371 |
+
|
| 372 |
+
if failures:
|
| 373 |
+
raise MultipleDoctestFailures(failures)
|
| 374 |
+
|
| 375 |
+
def _disable_output_capturing_for_darwin(self) -> None:
|
| 376 |
+
"""Disable output capturing. Otherwise, stdout is lost to ipdoctest (pytest#985)."""
|
| 377 |
+
if platform.system() != "Darwin":
|
| 378 |
+
return
|
| 379 |
+
capman = self.config.pluginmanager.getplugin("capturemanager")
|
| 380 |
+
if capman:
|
| 381 |
+
capman.suspend_global_capture(in_=True)
|
| 382 |
+
out, err = capman.read_global_capture()
|
| 383 |
+
sys.stdout.write(out)
|
| 384 |
+
sys.stderr.write(err)
|
| 385 |
+
|
| 386 |
+
# TODO: Type ignored -- breaks Liskov Substitution.
|
| 387 |
+
def repr_failure( # type: ignore[override]
|
| 388 |
+
self,
|
| 389 |
+
excinfo: ExceptionInfo[BaseException],
|
| 390 |
+
) -> Union[str, TerminalRepr]:
|
| 391 |
+
import doctest
|
| 392 |
+
|
| 393 |
+
failures: Optional[
|
| 394 |
+
Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
|
| 395 |
+
] = None
|
| 396 |
+
if isinstance(
|
| 397 |
+
excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
|
| 398 |
+
):
|
| 399 |
+
failures = [excinfo.value]
|
| 400 |
+
elif isinstance(excinfo.value, MultipleDoctestFailures):
|
| 401 |
+
failures = excinfo.value.failures
|
| 402 |
+
|
| 403 |
+
if failures is None:
|
| 404 |
+
return super().repr_failure(excinfo)
|
| 405 |
+
|
| 406 |
+
reprlocation_lines = []
|
| 407 |
+
for failure in failures:
|
| 408 |
+
example = failure.example
|
| 409 |
+
test = failure.test
|
| 410 |
+
filename = test.filename
|
| 411 |
+
if test.lineno is None:
|
| 412 |
+
lineno = None
|
| 413 |
+
else:
|
| 414 |
+
lineno = test.lineno + example.lineno + 1
|
| 415 |
+
message = type(failure).__name__
|
| 416 |
+
# TODO: ReprFileLocation doesn't expect a None lineno.
|
| 417 |
+
reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
|
| 418 |
+
checker = _get_checker()
|
| 419 |
+
report_choice = _get_report_choice(self.config.getoption("ipdoctestreport"))
|
| 420 |
+
if lineno is not None:
|
| 421 |
+
assert failure.test.docstring is not None
|
| 422 |
+
lines = failure.test.docstring.splitlines(False)
|
| 423 |
+
# add line numbers to the left of the error message
|
| 424 |
+
assert test.lineno is not None
|
| 425 |
+
lines = [
|
| 426 |
+
"%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
|
| 427 |
+
]
|
| 428 |
+
# trim docstring error lines to 10
|
| 429 |
+
lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
|
| 430 |
+
else:
|
| 431 |
+
lines = [
|
| 432 |
+
"EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
|
| 433 |
+
]
|
| 434 |
+
indent = ">>>"
|
| 435 |
+
for line in example.source.splitlines():
|
| 436 |
+
lines.append(f"??? {indent} {line}")
|
| 437 |
+
indent = "..."
|
| 438 |
+
if isinstance(failure, doctest.DocTestFailure):
|
| 439 |
+
lines += checker.output_difference(
|
| 440 |
+
example, failure.got, report_choice
|
| 441 |
+
).split("\n")
|
| 442 |
+
else:
|
| 443 |
+
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
|
| 444 |
+
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
|
| 445 |
+
lines += [
|
| 446 |
+
x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
|
| 447 |
+
]
|
| 448 |
+
reprlocation_lines.append((reprlocation, lines))
|
| 449 |
+
return ReprFailDoctest(reprlocation_lines)
|
| 450 |
+
|
| 451 |
+
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
| 452 |
+
assert self.dtest is not None
|
| 453 |
+
return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name
|
| 454 |
+
|
| 455 |
+
if pytest_version[0] < 7:
|
| 456 |
+
|
| 457 |
+
@property
|
| 458 |
+
def path(self) -> Path:
|
| 459 |
+
return Path(self.fspath)
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def _get_flag_lookup() -> Dict[str, int]:
|
| 463 |
+
import doctest
|
| 464 |
+
|
| 465 |
+
return dict(
|
| 466 |
+
DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
|
| 467 |
+
DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
|
| 468 |
+
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
|
| 469 |
+
ELLIPSIS=doctest.ELLIPSIS,
|
| 470 |
+
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
|
| 471 |
+
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
|
| 472 |
+
ALLOW_UNICODE=_get_allow_unicode_flag(),
|
| 473 |
+
ALLOW_BYTES=_get_allow_bytes_flag(),
|
| 474 |
+
NUMBER=_get_number_flag(),
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def get_optionflags(parent):
|
| 479 |
+
optionflags_str = parent.config.getini("ipdoctest_optionflags")
|
| 480 |
+
flag_lookup_table = _get_flag_lookup()
|
| 481 |
+
flag_acc = 0
|
| 482 |
+
for flag in optionflags_str:
|
| 483 |
+
flag_acc |= flag_lookup_table[flag]
|
| 484 |
+
return flag_acc
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def _get_continue_on_failure(config):
|
| 488 |
+
continue_on_failure = config.getvalue("ipdoctest_continue_on_failure")
|
| 489 |
+
if continue_on_failure:
|
| 490 |
+
# We need to turn off this if we use pdb since we should stop at
|
| 491 |
+
# the first failure.
|
| 492 |
+
if config.getvalue("usepdb"):
|
| 493 |
+
continue_on_failure = False
|
| 494 |
+
return continue_on_failure
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
class IPDoctestTextfile(pytest.Module):
|
| 498 |
+
obj = None
|
| 499 |
+
|
| 500 |
+
def collect(self) -> Iterable[IPDoctestItem]:
|
| 501 |
+
import doctest
|
| 502 |
+
from .ipdoctest import IPDocTestParser
|
| 503 |
+
|
| 504 |
+
# Inspired by doctest.testfile; ideally we would use it directly,
|
| 505 |
+
# but it doesn't support passing a custom checker.
|
| 506 |
+
encoding = self.config.getini("ipdoctest_encoding")
|
| 507 |
+
text = self.path.read_text(encoding)
|
| 508 |
+
filename = str(self.path)
|
| 509 |
+
name = self.path.name
|
| 510 |
+
globs = {"__name__": "__main__"}
|
| 511 |
+
|
| 512 |
+
optionflags = get_optionflags(self)
|
| 513 |
+
|
| 514 |
+
runner = _get_runner(
|
| 515 |
+
verbose=False,
|
| 516 |
+
optionflags=optionflags,
|
| 517 |
+
checker=_get_checker(),
|
| 518 |
+
continue_on_failure=_get_continue_on_failure(self.config),
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
parser = IPDocTestParser()
|
| 522 |
+
test = parser.get_doctest(text, globs, name, filename, 0)
|
| 523 |
+
if test.examples:
|
| 524 |
+
yield IPDoctestItem.from_parent(
|
| 525 |
+
self, name=test.name, runner=runner, dtest=test
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
if pytest_version[0] < 7:
|
| 529 |
+
|
| 530 |
+
@property
|
| 531 |
+
def path(self) -> Path:
|
| 532 |
+
return Path(self.fspath)
|
| 533 |
+
|
| 534 |
+
@classmethod
|
| 535 |
+
def from_parent(
|
| 536 |
+
cls,
|
| 537 |
+
parent,
|
| 538 |
+
*,
|
| 539 |
+
fspath=None,
|
| 540 |
+
path: Optional[Path] = None,
|
| 541 |
+
**kw,
|
| 542 |
+
):
|
| 543 |
+
if path is not None:
|
| 544 |
+
import py.path
|
| 545 |
+
|
| 546 |
+
fspath = py.path.local(path)
|
| 547 |
+
return super().from_parent(parent=parent, fspath=fspath, **kw)
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def _check_all_skipped(test: "doctest.DocTest") -> None:
|
| 551 |
+
"""Raise pytest.skip() if all examples in the given DocTest have the SKIP
|
| 552 |
+
option set."""
|
| 553 |
+
import doctest
|
| 554 |
+
|
| 555 |
+
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
|
| 556 |
+
if all_skipped:
|
| 557 |
+
pytest.skip("all docstests skipped by +SKIP option")
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
def _is_mocked(obj: object) -> bool:
|
| 561 |
+
"""Return if an object is possibly a mock object by checking the
|
| 562 |
+
existence of a highly improbable attribute."""
|
| 563 |
+
return (
|
| 564 |
+
safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
|
| 565 |
+
is not None
|
| 566 |
+
)
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
@contextmanager
|
| 570 |
+
def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
|
| 571 |
+
"""Context manager which replaces ``inspect.unwrap`` with a version
|
| 572 |
+
that's aware of mock objects and doesn't recurse into them."""
|
| 573 |
+
real_unwrap = inspect.unwrap
|
| 574 |
+
|
| 575 |
+
def _mock_aware_unwrap(
|
| 576 |
+
func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
|
| 577 |
+
) -> Any:
|
| 578 |
+
try:
|
| 579 |
+
if stop is None or stop is _is_mocked:
|
| 580 |
+
return real_unwrap(func, stop=_is_mocked)
|
| 581 |
+
_stop = stop
|
| 582 |
+
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
|
| 583 |
+
except Exception as e:
|
| 584 |
+
warnings.warn(
|
| 585 |
+
"Got %r when unwrapping %r. This is usually caused "
|
| 586 |
+
"by a violation of Python's object protocol; see e.g. "
|
| 587 |
+
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
|
| 588 |
+
PytestWarning,
|
| 589 |
+
)
|
| 590 |
+
raise
|
| 591 |
+
|
| 592 |
+
inspect.unwrap = _mock_aware_unwrap
|
| 593 |
+
try:
|
| 594 |
+
yield
|
| 595 |
+
finally:
|
| 596 |
+
inspect.unwrap = real_unwrap
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
class IPDoctestModule(pytest.Module):
|
| 600 |
+
def collect(self) -> Iterable[IPDoctestItem]:
|
| 601 |
+
import doctest
|
| 602 |
+
from .ipdoctest import DocTestFinder, IPDocTestParser
|
| 603 |
+
|
| 604 |
+
class MockAwareDocTestFinder(DocTestFinder):
|
| 605 |
+
"""A hackish ipdoctest finder that overrides stdlib internals to fix a stdlib bug.
|
| 606 |
+
|
| 607 |
+
https://github.com/pytest-dev/pytest/issues/3456
|
| 608 |
+
https://bugs.python.org/issue25532
|
| 609 |
+
"""
|
| 610 |
+
|
| 611 |
+
def _find_lineno(self, obj, source_lines):
|
| 612 |
+
"""Doctest code does not take into account `@property`, this
|
| 613 |
+
is a hackish way to fix it. https://bugs.python.org/issue17446
|
| 614 |
+
|
| 615 |
+
Wrapped Doctests will need to be unwrapped so the correct
|
| 616 |
+
line number is returned. This will be reported upstream. #8796
|
| 617 |
+
"""
|
| 618 |
+
if isinstance(obj, property):
|
| 619 |
+
obj = getattr(obj, "fget", obj)
|
| 620 |
+
|
| 621 |
+
if hasattr(obj, "__wrapped__"):
|
| 622 |
+
# Get the main obj in case of it being wrapped
|
| 623 |
+
obj = inspect.unwrap(obj)
|
| 624 |
+
|
| 625 |
+
# Type ignored because this is a private function.
|
| 626 |
+
return super()._find_lineno( # type:ignore[misc]
|
| 627 |
+
obj,
|
| 628 |
+
source_lines,
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
def _find(
|
| 632 |
+
self, tests, obj, name, module, source_lines, globs, seen
|
| 633 |
+
) -> None:
|
| 634 |
+
if _is_mocked(obj):
|
| 635 |
+
return
|
| 636 |
+
with _patch_unwrap_mock_aware():
|
| 637 |
+
# Type ignored because this is a private function.
|
| 638 |
+
super()._find( # type:ignore[misc]
|
| 639 |
+
tests, obj, name, module, source_lines, globs, seen
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
if self.path.name == "conftest.py":
|
| 643 |
+
if pytest_version[0] < 7:
|
| 644 |
+
module = self.config.pluginmanager._importconftest(
|
| 645 |
+
self.path,
|
| 646 |
+
self.config.getoption("importmode"),
|
| 647 |
+
)
|
| 648 |
+
else:
|
| 649 |
+
kwargs = {"rootpath": self.config.rootpath}
|
| 650 |
+
if pytest_version >= (8, 1):
|
| 651 |
+
kwargs["consider_namespace_packages"] = False
|
| 652 |
+
module = self.config.pluginmanager._importconftest(
|
| 653 |
+
self.path,
|
| 654 |
+
self.config.getoption("importmode"),
|
| 655 |
+
**kwargs,
|
| 656 |
+
)
|
| 657 |
+
else:
|
| 658 |
+
try:
|
| 659 |
+
kwargs = {"root": self.config.rootpath}
|
| 660 |
+
if pytest_version >= (8, 1):
|
| 661 |
+
kwargs["consider_namespace_packages"] = False
|
| 662 |
+
module = import_path(self.path, **kwargs)
|
| 663 |
+
except ImportError:
|
| 664 |
+
if self.config.getvalue("ipdoctest_ignore_import_errors"):
|
| 665 |
+
pytest.skip("unable to import module %r" % self.path)
|
| 666 |
+
else:
|
| 667 |
+
raise
|
| 668 |
+
# Uses internal doctest module parsing mechanism.
|
| 669 |
+
finder = MockAwareDocTestFinder(parser=IPDocTestParser())
|
| 670 |
+
optionflags = get_optionflags(self)
|
| 671 |
+
runner = _get_runner(
|
| 672 |
+
verbose=False,
|
| 673 |
+
optionflags=optionflags,
|
| 674 |
+
checker=_get_checker(),
|
| 675 |
+
continue_on_failure=_get_continue_on_failure(self.config),
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
for test in finder.find(module, module.__name__):
|
| 679 |
+
if test.examples: # skip empty ipdoctests
|
| 680 |
+
yield IPDoctestItem.from_parent(
|
| 681 |
+
self, name=test.name, runner=runner, dtest=test
|
| 682 |
+
)
|
| 683 |
+
|
| 684 |
+
if pytest_version[0] < 7:
|
| 685 |
+
|
| 686 |
+
@property
|
| 687 |
+
def path(self) -> Path:
|
| 688 |
+
return Path(self.fspath)
|
| 689 |
+
|
| 690 |
+
@classmethod
|
| 691 |
+
def from_parent(
|
| 692 |
+
cls,
|
| 693 |
+
parent,
|
| 694 |
+
*,
|
| 695 |
+
fspath=None,
|
| 696 |
+
path: Optional[Path] = None,
|
| 697 |
+
**kw,
|
| 698 |
+
):
|
| 699 |
+
if path is not None:
|
| 700 |
+
import py.path
|
| 701 |
+
|
| 702 |
+
fspath = py.path.local(path)
|
| 703 |
+
return super().from_parent(parent=parent, fspath=fspath, **kw)
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest:
|
| 707 |
+
"""Used by IPDoctestTextfile and IPDoctestItem to setup fixture information."""
|
| 708 |
+
|
| 709 |
+
def func() -> None:
|
| 710 |
+
pass
|
| 711 |
+
|
| 712 |
+
doctest_item.funcargs = {} # type: ignore[attr-defined]
|
| 713 |
+
fm = doctest_item.session._fixturemanager
|
| 714 |
+
kwargs = {"node": doctest_item, "func": func, "cls": None}
|
| 715 |
+
if pytest_version <= (8, 0):
|
| 716 |
+
kwargs["funcargs"] = False
|
| 717 |
+
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
| 718 |
+
**kwargs
|
| 719 |
+
)
|
| 720 |
+
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
|
| 721 |
+
if pytest_version <= (8, 0):
|
| 722 |
+
fixture_request._fillfixtures()
|
| 723 |
+
return fixture_request
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
def _init_checker_class() -> Type["IPDoctestOutputChecker"]:
|
| 727 |
+
import doctest
|
| 728 |
+
import re
|
| 729 |
+
from .ipdoctest import IPDoctestOutputChecker
|
| 730 |
+
|
| 731 |
+
class LiteralsOutputChecker(IPDoctestOutputChecker):
|
| 732 |
+
# Based on doctest_nose_plugin.py from the nltk project
|
| 733 |
+
# (https://github.com/nltk/nltk) and on the "numtest" doctest extension
|
| 734 |
+
# by Sebastien Boisgerault (https://github.com/boisgera/numtest).
|
| 735 |
+
|
| 736 |
+
_unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
|
| 737 |
+
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
|
| 738 |
+
_number_re = re.compile(
|
| 739 |
+
r"""
|
| 740 |
+
(?P<number>
|
| 741 |
+
(?P<mantissa>
|
| 742 |
+
(?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
|
| 743 |
+
|
|
| 744 |
+
(?P<integer2> [+-]?\d+)\.
|
| 745 |
+
)
|
| 746 |
+
(?:
|
| 747 |
+
[Ee]
|
| 748 |
+
(?P<exponent1> [+-]?\d+)
|
| 749 |
+
)?
|
| 750 |
+
|
|
| 751 |
+
(?P<integer3> [+-]?\d+)
|
| 752 |
+
(?:
|
| 753 |
+
[Ee]
|
| 754 |
+
(?P<exponent2> [+-]?\d+)
|
| 755 |
+
)
|
| 756 |
+
)
|
| 757 |
+
""",
|
| 758 |
+
re.VERBOSE,
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
def check_output(self, want: str, got: str, optionflags: int) -> bool:
|
| 762 |
+
if super().check_output(want, got, optionflags):
|
| 763 |
+
return True
|
| 764 |
+
|
| 765 |
+
allow_unicode = optionflags & _get_allow_unicode_flag()
|
| 766 |
+
allow_bytes = optionflags & _get_allow_bytes_flag()
|
| 767 |
+
allow_number = optionflags & _get_number_flag()
|
| 768 |
+
|
| 769 |
+
if not allow_unicode and not allow_bytes and not allow_number:
|
| 770 |
+
return False
|
| 771 |
+
|
| 772 |
+
def remove_prefixes(regex: Pattern[str], txt: str) -> str:
|
| 773 |
+
return re.sub(regex, r"\1\2", txt)
|
| 774 |
+
|
| 775 |
+
if allow_unicode:
|
| 776 |
+
want = remove_prefixes(self._unicode_literal_re, want)
|
| 777 |
+
got = remove_prefixes(self._unicode_literal_re, got)
|
| 778 |
+
|
| 779 |
+
if allow_bytes:
|
| 780 |
+
want = remove_prefixes(self._bytes_literal_re, want)
|
| 781 |
+
got = remove_prefixes(self._bytes_literal_re, got)
|
| 782 |
+
|
| 783 |
+
if allow_number:
|
| 784 |
+
got = self._remove_unwanted_precision(want, got)
|
| 785 |
+
|
| 786 |
+
return super().check_output(want, got, optionflags)
|
| 787 |
+
|
| 788 |
+
def _remove_unwanted_precision(self, want: str, got: str) -> str:
|
| 789 |
+
wants = list(self._number_re.finditer(want))
|
| 790 |
+
gots = list(self._number_re.finditer(got))
|
| 791 |
+
if len(wants) != len(gots):
|
| 792 |
+
return got
|
| 793 |
+
offset = 0
|
| 794 |
+
for w, g in zip(wants, gots):
|
| 795 |
+
fraction: Optional[str] = w.group("fraction")
|
| 796 |
+
exponent: Optional[str] = w.group("exponent1")
|
| 797 |
+
if exponent is None:
|
| 798 |
+
exponent = w.group("exponent2")
|
| 799 |
+
precision = 0 if fraction is None else len(fraction)
|
| 800 |
+
if exponent is not None:
|
| 801 |
+
precision -= int(exponent)
|
| 802 |
+
if float(w.group()) == approx(float(g.group()), abs=10**-precision):
|
| 803 |
+
# They're close enough. Replace the text we actually
|
| 804 |
+
# got with the text we want, so that it will match when we
|
| 805 |
+
# check the string literally.
|
| 806 |
+
got = (
|
| 807 |
+
got[: g.start() + offset] + w.group() + got[g.end() + offset :]
|
| 808 |
+
)
|
| 809 |
+
offset += w.end() - w.start() - (g.end() - g.start())
|
| 810 |
+
return got
|
| 811 |
+
|
| 812 |
+
return LiteralsOutputChecker
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
def _get_checker() -> "IPDoctestOutputChecker":
|
| 816 |
+
"""Return a IPDoctestOutputChecker subclass that supports some
|
| 817 |
+
additional options:
|
| 818 |
+
|
| 819 |
+
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
|
| 820 |
+
prefixes (respectively) in string literals. Useful when the same
|
| 821 |
+
ipdoctest should run in Python 2 and Python 3.
|
| 822 |
+
|
| 823 |
+
* NUMBER to ignore floating-point differences smaller than the
|
| 824 |
+
precision of the literal number in the ipdoctest.
|
| 825 |
+
|
| 826 |
+
An inner class is used to avoid importing "ipdoctest" at the module
|
| 827 |
+
level.
|
| 828 |
+
"""
|
| 829 |
+
global CHECKER_CLASS
|
| 830 |
+
if CHECKER_CLASS is None:
|
| 831 |
+
CHECKER_CLASS = _init_checker_class()
|
| 832 |
+
return CHECKER_CLASS()
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
def _get_allow_unicode_flag() -> int:
|
| 836 |
+
"""Register and return the ALLOW_UNICODE flag."""
|
| 837 |
+
import doctest
|
| 838 |
+
|
| 839 |
+
return doctest.register_optionflag("ALLOW_UNICODE")
|
| 840 |
+
|
| 841 |
+
|
| 842 |
+
def _get_allow_bytes_flag() -> int:
|
| 843 |
+
"""Register and return the ALLOW_BYTES flag."""
|
| 844 |
+
import doctest
|
| 845 |
+
|
| 846 |
+
return doctest.register_optionflag("ALLOW_BYTES")
|
| 847 |
+
|
| 848 |
+
|
| 849 |
+
def _get_number_flag() -> int:
|
| 850 |
+
"""Register and return the NUMBER flag."""
|
| 851 |
+
import doctest
|
| 852 |
+
|
| 853 |
+
return doctest.register_optionflag("NUMBER")
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
def _get_report_choice(key: str) -> int:
|
| 857 |
+
"""Return the actual `ipdoctest` module flag value.
|
| 858 |
+
|
| 859 |
+
We want to do it as late as possible to avoid importing `ipdoctest` and all
|
| 860 |
+
its dependencies when parsing options, as it adds overhead and breaks tests.
|
| 861 |
+
"""
|
| 862 |
+
import doctest
|
| 863 |
+
|
| 864 |
+
return {
|
| 865 |
+
DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
|
| 866 |
+
DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
|
| 867 |
+
DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
|
| 868 |
+
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
|
| 869 |
+
DOCTEST_REPORT_CHOICE_NONE: 0,
|
| 870 |
+
}[key]
|
| 871 |
+
|
| 872 |
+
|
| 873 |
+
@pytest.fixture(scope="session")
|
| 874 |
+
def ipdoctest_namespace() -> Dict[str, Any]:
|
| 875 |
+
"""Fixture that returns a :py:class:`dict` that will be injected into the
|
| 876 |
+
namespace of ipdoctests."""
|
| 877 |
+
return dict()
|
lib/python3.12/site-packages/IPython/testing/plugin/setup.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""A Nose plugin to support IPython doctests.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from setuptools import setup
|
| 6 |
+
|
| 7 |
+
setup(name='IPython doctest plugin',
|
| 8 |
+
version='0.1',
|
| 9 |
+
author='The IPython Team',
|
| 10 |
+
description = 'Nose plugin to load IPython-extended doctests',
|
| 11 |
+
license = 'LGPL',
|
| 12 |
+
py_modules = ['ipdoctest'],
|
| 13 |
+
entry_points = {
|
| 14 |
+
'nose.plugins.0.10': ['ipdoctest = ipdoctest:IPythonDoctest',
|
| 15 |
+
'extdoctest = ipdoctest:ExtensionDoctest',
|
| 16 |
+
],
|
| 17 |
+
},
|
| 18 |
+
)
|
lib/python3.12/site-packages/IPython/testing/plugin/simple.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Simple example using doctests.
|
| 2 |
+
|
| 3 |
+
This file just contains doctests both using plain python and IPython prompts.
|
| 4 |
+
All tests should be loaded by Pytest.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def pyfunc():
|
| 9 |
+
"""Some pure python tests...
|
| 10 |
+
|
| 11 |
+
>>> pyfunc()
|
| 12 |
+
'pyfunc'
|
| 13 |
+
|
| 14 |
+
>>> import os
|
| 15 |
+
|
| 16 |
+
>>> 2+3
|
| 17 |
+
5
|
| 18 |
+
|
| 19 |
+
>>> for i in range(3):
|
| 20 |
+
... print(i, end=' ')
|
| 21 |
+
... print(i+1, end=' ')
|
| 22 |
+
...
|
| 23 |
+
0 1 1 2 2 3
|
| 24 |
+
"""
|
| 25 |
+
return "pyfunc"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def ipyfunc():
|
| 29 |
+
"""Some IPython tests...
|
| 30 |
+
|
| 31 |
+
In [1]: ipyfunc()
|
| 32 |
+
Out[1]: 'ipyfunc'
|
| 33 |
+
|
| 34 |
+
In [2]: import os
|
| 35 |
+
|
| 36 |
+
In [3]: 2+3
|
| 37 |
+
Out[3]: 5
|
| 38 |
+
|
| 39 |
+
In [4]: for i in range(3):
|
| 40 |
+
...: print(i, end=' ')
|
| 41 |
+
...: print(i+1, end=' ')
|
| 42 |
+
...:
|
| 43 |
+
Out[4]: 0 1 1 2 2 3
|
| 44 |
+
"""
|
| 45 |
+
return "ipyfunc"
|
lib/python3.12/site-packages/IPython/testing/plugin/test_combo.txt
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=======================
|
| 2 |
+
Combo testing example
|
| 3 |
+
=======================
|
| 4 |
+
|
| 5 |
+
This is a simple example that mixes ipython doctests::
|
| 6 |
+
|
| 7 |
+
In [1]: import code
|
| 8 |
+
|
| 9 |
+
In [2]: 2**12
|
| 10 |
+
Out[2]: 4096
|
| 11 |
+
|
| 12 |
+
with command-line example information that does *not* get executed::
|
| 13 |
+
|
| 14 |
+
$ mpirun -n 4 ipengine --controller-port=10000 --controller-ip=host0
|
| 15 |
+
|
| 16 |
+
and with literal examples of Python source code::
|
| 17 |
+
|
| 18 |
+
controller = dict(host='myhost',
|
| 19 |
+
engine_port=None, # default is 10105
|
| 20 |
+
control_port=None,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# keys are hostnames, values are the number of engine on that host
|
| 24 |
+
engines = dict(node1=2,
|
| 25 |
+
node2=2,
|
| 26 |
+
node3=2,
|
| 27 |
+
node3=2,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
# Force failure to detect that this test is being run.
|
| 31 |
+
1/0
|
| 32 |
+
|
| 33 |
+
These source code examples are executed but no output is compared at all. An
|
| 34 |
+
error or failure is reported only if an exception is raised.
|
| 35 |
+
|
| 36 |
+
NOTE: the execution of pure python blocks is not yet working!
|
lib/python3.12/site-packages/IPython/testing/plugin/test_exampleip.txt
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
=================================
|
| 2 |
+
Tests in example form - IPython
|
| 3 |
+
=================================
|
| 4 |
+
|
| 5 |
+
You can write text files with examples that use IPython prompts (as long as you
|
| 6 |
+
use the nose ipython doctest plugin), but you can not mix and match prompt
|
| 7 |
+
styles in a single file. That is, you either use all ``>>>`` prompts or all
|
| 8 |
+
IPython-style prompts. Your test suite *can* have both types, you just need to
|
| 9 |
+
put each type of example in a separate. Using IPython prompts, you can paste
|
| 10 |
+
directly from your session::
|
| 11 |
+
|
| 12 |
+
In [5]: s="Hello World"
|
| 13 |
+
|
| 14 |
+
In [6]: s.upper()
|
| 15 |
+
Out[6]: 'HELLO WORLD'
|
| 16 |
+
|
| 17 |
+
Another example::
|
| 18 |
+
|
| 19 |
+
In [8]: 1+3
|
| 20 |
+
Out[8]: 4
|
| 21 |
+
|
| 22 |
+
Just like in IPython docstrings, you can use all IPython syntax and features::
|
| 23 |
+
|
| 24 |
+
In [9]: !echo hello
|
| 25 |
+
hello
|
| 26 |
+
|
| 27 |
+
In [10]: a='hi'
|
| 28 |
+
|
| 29 |
+
In [11]: !echo $a
|
| 30 |
+
hi
|
lib/python3.12/site-packages/IPython/testing/plugin/test_ipdoctest.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the ipdoctest machinery itself.
|
| 2 |
+
|
| 3 |
+
Note: in a file named test_X, functions whose only test is their docstring (as
|
| 4 |
+
a doctest) and which have no test functionality of their own, should be called
|
| 5 |
+
'doctest_foo' instead of 'test_foo', otherwise they get double-counted (the
|
| 6 |
+
empty function call is counted as a test, which just inflates tests numbers
|
| 7 |
+
artificially).
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
def doctest_simple():
|
| 11 |
+
"""ipdoctest must handle simple inputs
|
| 12 |
+
|
| 13 |
+
In [1]: 1
|
| 14 |
+
Out[1]: 1
|
| 15 |
+
|
| 16 |
+
In [2]: print(1)
|
| 17 |
+
1
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def doctest_multiline1():
|
| 21 |
+
"""The ipdoctest machinery must handle multiline examples gracefully.
|
| 22 |
+
|
| 23 |
+
In [2]: for i in range(4):
|
| 24 |
+
...: print(i)
|
| 25 |
+
...:
|
| 26 |
+
0
|
| 27 |
+
1
|
| 28 |
+
2
|
| 29 |
+
3
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def doctest_multiline2():
|
| 33 |
+
"""Multiline examples that define functions and print output.
|
| 34 |
+
|
| 35 |
+
In [7]: def f(x):
|
| 36 |
+
...: return x+1
|
| 37 |
+
...:
|
| 38 |
+
|
| 39 |
+
In [8]: f(1)
|
| 40 |
+
Out[8]: 2
|
| 41 |
+
|
| 42 |
+
In [9]: def g(x):
|
| 43 |
+
...: print('x is:',x)
|
| 44 |
+
...:
|
| 45 |
+
|
| 46 |
+
In [10]: g(1)
|
| 47 |
+
x is: 1
|
| 48 |
+
|
| 49 |
+
In [11]: g('hello')
|
| 50 |
+
x is: hello
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def doctest_multiline3():
|
| 55 |
+
"""Multiline examples with blank lines.
|
| 56 |
+
|
| 57 |
+
In [12]: def h(x):
|
| 58 |
+
....: if x>1:
|
| 59 |
+
....: return x**2
|
| 60 |
+
....: # To leave a blank line in the input, you must mark it
|
| 61 |
+
....: # with a comment character:
|
| 62 |
+
....: #
|
| 63 |
+
....: # otherwise the doctest parser gets confused.
|
| 64 |
+
....: else:
|
| 65 |
+
....: return -1
|
| 66 |
+
....:
|
| 67 |
+
|
| 68 |
+
In [13]: h(5)
|
| 69 |
+
Out[13]: 25
|
| 70 |
+
|
| 71 |
+
In [14]: h(1)
|
| 72 |
+
Out[14]: -1
|
| 73 |
+
|
| 74 |
+
In [15]: h(0)
|
| 75 |
+
Out[15]: -1
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def doctest_builtin_underscore():
|
| 80 |
+
"""Defining builtins._ should not break anything outside the doctest
|
| 81 |
+
while also should be working as expected inside the doctest.
|
| 82 |
+
|
| 83 |
+
In [1]: import builtins
|
| 84 |
+
|
| 85 |
+
In [2]: builtins._ = 42
|
| 86 |
+
|
| 87 |
+
In [3]: builtins._
|
| 88 |
+
Out[3]: 42
|
| 89 |
+
|
| 90 |
+
In [4]: _
|
| 91 |
+
Out[4]: 42
|
| 92 |
+
"""
|
lib/python3.12/site-packages/IPython/testing/plugin/test_refs.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Some simple tests for the plugin while running scripts.
|
| 2 |
+
"""
|
| 3 |
+
# Module imports
|
| 4 |
+
# Std lib
|
| 5 |
+
import inspect
|
| 6 |
+
|
| 7 |
+
# Our own
|
| 8 |
+
|
| 9 |
+
#-----------------------------------------------------------------------------
|
| 10 |
+
# Testing functions
|
| 11 |
+
|
| 12 |
+
def test_trivial():
|
| 13 |
+
"""A trivial passing test."""
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
def doctest_run():
|
| 17 |
+
"""Test running a trivial script.
|
| 18 |
+
|
| 19 |
+
In [13]: run simplevars.py
|
| 20 |
+
x is: 1
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def doctest_runvars():
|
| 24 |
+
"""Test that variables defined in scripts get loaded correctly via %run.
|
| 25 |
+
|
| 26 |
+
In [13]: run simplevars.py
|
| 27 |
+
x is: 1
|
| 28 |
+
|
| 29 |
+
In [14]: x
|
| 30 |
+
Out[14]: 1
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
def doctest_ivars():
|
| 34 |
+
"""Test that variables defined interactively are picked up.
|
| 35 |
+
In [5]: zz=1
|
| 36 |
+
|
| 37 |
+
In [6]: zz
|
| 38 |
+
Out[6]: 1
|
| 39 |
+
"""
|
lib/python3.12/site-packages/IPython/testing/skipdoctest.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Decorators marks that a doctest should be skipped.
|
| 2 |
+
|
| 3 |
+
The IPython.testing.decorators module triggers various extra imports, including
|
| 4 |
+
numpy and sympy if they're present. Since this decorator is used in core parts
|
| 5 |
+
of IPython, it's in a separate module so that running IPython doesn't trigger
|
| 6 |
+
those imports."""
|
| 7 |
+
|
| 8 |
+
# Copyright (C) IPython Development Team
|
| 9 |
+
# Distributed under the terms of the Modified BSD License.
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def skip_doctest(f):
|
| 13 |
+
"""Decorator - mark a function or method for skipping its doctest.
|
| 14 |
+
|
| 15 |
+
This decorator allows you to mark a function whose docstring you wish to
|
| 16 |
+
omit from testing, while preserving the docstring for introspection, help,
|
| 17 |
+
etc."""
|
| 18 |
+
f.__skip_doctest__ = True
|
| 19 |
+
return f
|
lib/python3.12/site-packages/IPython/testing/tools.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generic testing tools.
|
| 2 |
+
|
| 3 |
+
Authors
|
| 4 |
+
-------
|
| 5 |
+
- Fernando Perez <Fernando.Perez@berkeley.edu>
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# Copyright (c) IPython Development Team.
|
| 10 |
+
# Distributed under the terms of the Modified BSD License.
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import re
|
| 15 |
+
import sys
|
| 16 |
+
import tempfile
|
| 17 |
+
import unittest
|
| 18 |
+
|
| 19 |
+
from contextlib import contextmanager
|
| 20 |
+
from io import StringIO
|
| 21 |
+
from subprocess import Popen, PIPE
|
| 22 |
+
from unittest.mock import patch
|
| 23 |
+
|
| 24 |
+
from traitlets.config.loader import Config
|
| 25 |
+
from IPython.utils.process import get_output_error_code
|
| 26 |
+
from IPython.utils.text import list_strings
|
| 27 |
+
from IPython.utils.io import temp_pyfile, Tee
|
| 28 |
+
from IPython.utils import py3compat
|
| 29 |
+
|
| 30 |
+
from . import decorators as dec
|
| 31 |
+
from . import skipdoctest
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
# The docstring for full_path doctests differently on win32 (different path
|
| 35 |
+
# separator) so just skip the doctest there. The example remains informative.
|
| 36 |
+
doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
|
| 37 |
+
|
| 38 |
+
@doctest_deco
|
| 39 |
+
def full_path(startPath: str, files: list[str]) -> list[str]:
|
| 40 |
+
"""Make full paths for all the listed files, based on startPath.
|
| 41 |
+
|
| 42 |
+
Only the base part of startPath is kept, since this routine is typically
|
| 43 |
+
used with a script's ``__file__`` variable as startPath. The base of startPath
|
| 44 |
+
is then prepended to all the listed files, forming the output list.
|
| 45 |
+
|
| 46 |
+
Parameters
|
| 47 |
+
----------
|
| 48 |
+
startPath : string
|
| 49 |
+
Initial path to use as the base for the results. This path is split
|
| 50 |
+
using os.path.split() and only its first component is kept.
|
| 51 |
+
|
| 52 |
+
files : list
|
| 53 |
+
One or more files.
|
| 54 |
+
|
| 55 |
+
Examples
|
| 56 |
+
--------
|
| 57 |
+
|
| 58 |
+
>>> full_path('/foo/bar.py',['a.txt','b.txt'])
|
| 59 |
+
['/foo/a.txt', '/foo/b.txt']
|
| 60 |
+
|
| 61 |
+
>>> full_path('/foo',['a.txt','b.txt'])
|
| 62 |
+
['/a.txt', '/b.txt']
|
| 63 |
+
|
| 64 |
+
"""
|
| 65 |
+
assert isinstance(files, list)
|
| 66 |
+
base = os.path.split(startPath)[0]
|
| 67 |
+
return [ os.path.join(base,f) for f in files ]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def parse_test_output(txt):
|
| 71 |
+
"""Parse the output of a test run and return errors, failures.
|
| 72 |
+
|
| 73 |
+
Parameters
|
| 74 |
+
----------
|
| 75 |
+
txt : str
|
| 76 |
+
Text output of a test run, assumed to contain a line of one of the
|
| 77 |
+
following forms::
|
| 78 |
+
|
| 79 |
+
'FAILED (errors=1)'
|
| 80 |
+
'FAILED (failures=1)'
|
| 81 |
+
'FAILED (errors=1, failures=1)'
|
| 82 |
+
|
| 83 |
+
Returns
|
| 84 |
+
-------
|
| 85 |
+
nerr, nfail
|
| 86 |
+
number of errors and failures.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
|
| 90 |
+
if err_m:
|
| 91 |
+
nerr = int(err_m.group(1))
|
| 92 |
+
nfail = 0
|
| 93 |
+
return nerr, nfail
|
| 94 |
+
|
| 95 |
+
fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
|
| 96 |
+
if fail_m:
|
| 97 |
+
nerr = 0
|
| 98 |
+
nfail = int(fail_m.group(1))
|
| 99 |
+
return nerr, nfail
|
| 100 |
+
|
| 101 |
+
both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
|
| 102 |
+
re.MULTILINE)
|
| 103 |
+
if both_m:
|
| 104 |
+
nerr = int(both_m.group(1))
|
| 105 |
+
nfail = int(both_m.group(2))
|
| 106 |
+
return nerr, nfail
|
| 107 |
+
|
| 108 |
+
# If the input didn't match any of these forms, assume no error/failures
|
| 109 |
+
return 0, 0
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# So nose doesn't think this is a test
|
| 113 |
+
parse_test_output.__test__ = False
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def default_argv():
|
| 117 |
+
"""Return a valid default argv for creating testing instances of ipython"""
|
| 118 |
+
|
| 119 |
+
return [
|
| 120 |
+
"--quick", # so no config file is loaded
|
| 121 |
+
# Other defaults to minimize side effects on stdout
|
| 122 |
+
"--colors=nocolor",
|
| 123 |
+
"--no-term-title",
|
| 124 |
+
"--no-banner",
|
| 125 |
+
"--autocall=0",
|
| 126 |
+
]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def default_config():
|
| 130 |
+
"""Return a config object with good defaults for testing."""
|
| 131 |
+
config = Config()
|
| 132 |
+
config.TerminalInteractiveShell.colors = "nocolor"
|
| 133 |
+
config.TerminalTerminalInteractiveShell.term_title = (False,)
|
| 134 |
+
config.TerminalInteractiveShell.autocall = 0
|
| 135 |
+
f = tempfile.NamedTemporaryFile(suffix="test_hist.sqlite", delete=False)
|
| 136 |
+
config.HistoryManager.hist_file = Path(f.name)
|
| 137 |
+
f.close()
|
| 138 |
+
config.HistoryManager.db_cache_size = 10000
|
| 139 |
+
return config
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def get_ipython_cmd(as_string=False):
|
| 143 |
+
"""
|
| 144 |
+
Return appropriate IPython command line name. By default, this will return
|
| 145 |
+
a list that can be used with subprocess.Popen, for example, but passing
|
| 146 |
+
`as_string=True` allows for returning the IPython command as a string.
|
| 147 |
+
|
| 148 |
+
Parameters
|
| 149 |
+
----------
|
| 150 |
+
as_string: bool
|
| 151 |
+
Flag to allow to return the command as a string.
|
| 152 |
+
"""
|
| 153 |
+
ipython_cmd = [sys.executable, "-m", "IPython"]
|
| 154 |
+
|
| 155 |
+
if as_string:
|
| 156 |
+
ipython_cmd = " ".join(ipython_cmd)
|
| 157 |
+
|
| 158 |
+
return ipython_cmd
|
| 159 |
+
|
| 160 |
+
def ipexec(fname, options=None, commands=()):
|
| 161 |
+
"""Utility to call 'ipython filename'.
|
| 162 |
+
|
| 163 |
+
Starts IPython with a minimal and safe configuration to make startup as fast
|
| 164 |
+
as possible.
|
| 165 |
+
|
| 166 |
+
Note that this starts IPython in a subprocess!
|
| 167 |
+
|
| 168 |
+
Parameters
|
| 169 |
+
----------
|
| 170 |
+
fname : str, Path
|
| 171 |
+
Name of file to be executed (should have .py or .ipy extension).
|
| 172 |
+
|
| 173 |
+
options : optional, list
|
| 174 |
+
Extra command-line flags to be passed to IPython.
|
| 175 |
+
|
| 176 |
+
commands : optional, list
|
| 177 |
+
Commands to send in on stdin
|
| 178 |
+
|
| 179 |
+
Returns
|
| 180 |
+
-------
|
| 181 |
+
``(stdout, stderr)`` of ipython subprocess.
|
| 182 |
+
"""
|
| 183 |
+
__tracebackhide__ = True
|
| 184 |
+
|
| 185 |
+
if options is None:
|
| 186 |
+
options = []
|
| 187 |
+
|
| 188 |
+
cmdargs = default_argv() + options
|
| 189 |
+
|
| 190 |
+
test_dir = os.path.dirname(__file__)
|
| 191 |
+
|
| 192 |
+
ipython_cmd = get_ipython_cmd()
|
| 193 |
+
# Absolute path for filename
|
| 194 |
+
full_fname = os.path.join(test_dir, fname)
|
| 195 |
+
full_cmd = ipython_cmd + cmdargs + ['--', full_fname]
|
| 196 |
+
env = os.environ.copy()
|
| 197 |
+
# FIXME: ignore all warnings in ipexec while we have shims
|
| 198 |
+
# should we keep suppressing warnings here, even after removing shims?
|
| 199 |
+
env['PYTHONWARNINGS'] = 'ignore'
|
| 200 |
+
# env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
|
| 201 |
+
# Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout)
|
| 202 |
+
env.pop("PYCHARM_HOSTED", None)
|
| 203 |
+
for k, v in env.items():
|
| 204 |
+
# Debug a bizarre failure we've seen on Windows:
|
| 205 |
+
# TypeError: environment can only contain strings
|
| 206 |
+
if not isinstance(v, str):
|
| 207 |
+
print(k, v)
|
| 208 |
+
p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
|
| 209 |
+
out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
|
| 210 |
+
out, err = py3compat.decode(out), py3compat.decode(err)
|
| 211 |
+
# `import readline` causes 'ESC[?1034h' to be output sometimes,
|
| 212 |
+
# so strip that out before doing comparisons
|
| 213 |
+
if out:
|
| 214 |
+
out = re.sub(r'\x1b\[[^h]+h', '', out)
|
| 215 |
+
return out, err
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def ipexec_validate(fname, expected_out, expected_err='',
|
| 219 |
+
options=None, commands=()):
|
| 220 |
+
"""Utility to call 'ipython filename' and validate output/error.
|
| 221 |
+
|
| 222 |
+
This function raises an AssertionError if the validation fails.
|
| 223 |
+
|
| 224 |
+
Note that this starts IPython in a subprocess!
|
| 225 |
+
|
| 226 |
+
Parameters
|
| 227 |
+
----------
|
| 228 |
+
fname : str, Path
|
| 229 |
+
Name of the file to be executed (should have .py or .ipy extension).
|
| 230 |
+
|
| 231 |
+
expected_out : str
|
| 232 |
+
Expected stdout of the process.
|
| 233 |
+
|
| 234 |
+
expected_err : optional, str
|
| 235 |
+
Expected stderr of the process.
|
| 236 |
+
|
| 237 |
+
options : optional, list
|
| 238 |
+
Extra command-line flags to be passed to IPython.
|
| 239 |
+
|
| 240 |
+
Returns
|
| 241 |
+
-------
|
| 242 |
+
None
|
| 243 |
+
"""
|
| 244 |
+
__tracebackhide__ = True
|
| 245 |
+
|
| 246 |
+
out, err = ipexec(fname, options, commands)
|
| 247 |
+
# print('OUT', out) # dbg
|
| 248 |
+
# print('ERR', err) # dbg
|
| 249 |
+
# If there are any errors, we must check those before stdout, as they may be
|
| 250 |
+
# more informative than simply having an empty stdout.
|
| 251 |
+
if err:
|
| 252 |
+
if expected_err:
|
| 253 |
+
assert "\n".join(err.strip().splitlines()) == "\n".join(
|
| 254 |
+
expected_err.strip().splitlines()
|
| 255 |
+
)
|
| 256 |
+
else:
|
| 257 |
+
raise ValueError('Running file %r produced error: %r' %
|
| 258 |
+
(fname, err))
|
| 259 |
+
# If no errors or output on stderr was expected, match stdout
|
| 260 |
+
assert "\n".join(out.strip().splitlines()) == "\n".join(
|
| 261 |
+
expected_out.strip().splitlines()
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class TempFileMixin(unittest.TestCase):
|
| 266 |
+
"""Utility class to create temporary Python/IPython files.
|
| 267 |
+
|
| 268 |
+
Meant as a mixin class for test cases."""
|
| 269 |
+
|
| 270 |
+
def mktmp(self, src, ext='.py'):
|
| 271 |
+
"""Make a valid python temp file."""
|
| 272 |
+
fname = temp_pyfile(src, ext)
|
| 273 |
+
if not hasattr(self, 'tmps'):
|
| 274 |
+
self.tmps=[]
|
| 275 |
+
self.tmps.append(fname)
|
| 276 |
+
self.fname = fname
|
| 277 |
+
|
| 278 |
+
def tearDown(self):
|
| 279 |
+
# If the tmpfile wasn't made because of skipped tests, like in
|
| 280 |
+
# win32, there's nothing to cleanup.
|
| 281 |
+
if hasattr(self, 'tmps'):
|
| 282 |
+
for fname in self.tmps:
|
| 283 |
+
# If the tmpfile wasn't made because of skipped tests, like in
|
| 284 |
+
# win32, there's nothing to cleanup.
|
| 285 |
+
try:
|
| 286 |
+
os.unlink(fname)
|
| 287 |
+
except:
|
| 288 |
+
# On Windows, even though we close the file, we still can't
|
| 289 |
+
# delete it. I have no clue why
|
| 290 |
+
pass
|
| 291 |
+
|
| 292 |
+
def __enter__(self):
|
| 293 |
+
return self
|
| 294 |
+
|
| 295 |
+
def __exit__(self, exc_type, exc_value, traceback):
|
| 296 |
+
self.tearDown()
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
MyStringIO = StringIO
|
| 300 |
+
|
| 301 |
+
_re_type = type(re.compile(r''))
|
| 302 |
+
|
| 303 |
+
notprinted_msg = """Did not find {0!r} in printed output (on {1}):
|
| 304 |
+
-------
|
| 305 |
+
{2!s}
|
| 306 |
+
-------
|
| 307 |
+
"""
|
| 308 |
+
|
| 309 |
+
class AssertPrints:
|
| 310 |
+
"""Context manager for testing that code prints certain text.
|
| 311 |
+
|
| 312 |
+
Examples
|
| 313 |
+
--------
|
| 314 |
+
>>> with AssertPrints("abc", suppress=False):
|
| 315 |
+
... print("abcd")
|
| 316 |
+
... print("def")
|
| 317 |
+
...
|
| 318 |
+
abcd
|
| 319 |
+
def
|
| 320 |
+
"""
|
| 321 |
+
def __init__(self, s, channel='stdout', suppress=True):
|
| 322 |
+
self.s = s
|
| 323 |
+
if isinstance(self.s, (str, _re_type)):
|
| 324 |
+
self.s = [self.s]
|
| 325 |
+
self.channel = channel
|
| 326 |
+
self.suppress = suppress
|
| 327 |
+
|
| 328 |
+
def __enter__(self):
|
| 329 |
+
self.orig_stream = getattr(sys, self.channel)
|
| 330 |
+
self.buffer = MyStringIO()
|
| 331 |
+
self.tee = Tee(self.buffer, channel=self.channel)
|
| 332 |
+
setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
|
| 333 |
+
|
| 334 |
+
def __exit__(self, etype, value, traceback):
|
| 335 |
+
__tracebackhide__ = True
|
| 336 |
+
|
| 337 |
+
try:
|
| 338 |
+
if value is not None:
|
| 339 |
+
# If an error was raised, don't check anything else
|
| 340 |
+
return False
|
| 341 |
+
self.tee.flush()
|
| 342 |
+
setattr(sys, self.channel, self.orig_stream)
|
| 343 |
+
printed = self.buffer.getvalue()
|
| 344 |
+
for s in self.s:
|
| 345 |
+
if isinstance(s, _re_type):
|
| 346 |
+
assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
|
| 347 |
+
else:
|
| 348 |
+
assert s in printed, notprinted_msg.format(s, self.channel, printed)
|
| 349 |
+
return False
|
| 350 |
+
finally:
|
| 351 |
+
self.tee.close()
|
| 352 |
+
|
| 353 |
+
printed_msg = """Found {0!r} in printed output (on {1}):
|
| 354 |
+
-------
|
| 355 |
+
{2!s}
|
| 356 |
+
-------
|
| 357 |
+
"""
|
| 358 |
+
|
| 359 |
+
class AssertNotPrints(AssertPrints):
|
| 360 |
+
"""Context manager for checking that certain output *isn't* produced.
|
| 361 |
+
|
| 362 |
+
Counterpart of AssertPrints"""
|
| 363 |
+
def __exit__(self, etype, value, traceback):
|
| 364 |
+
__tracebackhide__ = True
|
| 365 |
+
|
| 366 |
+
try:
|
| 367 |
+
if value is not None:
|
| 368 |
+
# If an error was raised, don't check anything else
|
| 369 |
+
self.tee.close()
|
| 370 |
+
return False
|
| 371 |
+
self.tee.flush()
|
| 372 |
+
setattr(sys, self.channel, self.orig_stream)
|
| 373 |
+
printed = self.buffer.getvalue()
|
| 374 |
+
for s in self.s:
|
| 375 |
+
if isinstance(s, _re_type):
|
| 376 |
+
assert not s.search(printed),printed_msg.format(
|
| 377 |
+
s.pattern, self.channel, printed)
|
| 378 |
+
else:
|
| 379 |
+
assert s not in printed, printed_msg.format(
|
| 380 |
+
s, self.channel, printed)
|
| 381 |
+
return False
|
| 382 |
+
finally:
|
| 383 |
+
self.tee.close()
|
| 384 |
+
|
| 385 |
+
@contextmanager
|
| 386 |
+
def make_tempfile(name):
|
| 387 |
+
"""Create an empty, named, temporary file for the duration of the context."""
|
| 388 |
+
open(name, "w", encoding="utf-8").close()
|
| 389 |
+
try:
|
| 390 |
+
yield
|
| 391 |
+
finally:
|
| 392 |
+
os.unlink(name)
|
| 393 |
+
|
| 394 |
+
def fake_input(inputs):
|
| 395 |
+
"""Temporarily replace the input() function to return the given values
|
| 396 |
+
|
| 397 |
+
Use as a context manager:
|
| 398 |
+
|
| 399 |
+
with fake_input(['result1', 'result2']):
|
| 400 |
+
...
|
| 401 |
+
|
| 402 |
+
Values are returned in order. If input() is called again after the last value
|
| 403 |
+
was used, EOFError is raised.
|
| 404 |
+
"""
|
| 405 |
+
it = iter(inputs)
|
| 406 |
+
def mock_input(prompt=''):
|
| 407 |
+
try:
|
| 408 |
+
return next(it)
|
| 409 |
+
except StopIteration as e:
|
| 410 |
+
raise EOFError('No more inputs given') from e
|
| 411 |
+
|
| 412 |
+
return patch('builtins.input', mock_input)
|
| 413 |
+
|
| 414 |
+
def help_output_test(subcommand=''):
|
| 415 |
+
"""test that `ipython [subcommand] -h` works"""
|
| 416 |
+
cmd = get_ipython_cmd() + [subcommand, '-h']
|
| 417 |
+
out, err, rc = get_output_error_code(cmd)
|
| 418 |
+
assert rc == 0, err
|
| 419 |
+
assert "Traceback" not in err
|
| 420 |
+
assert "Options" in out
|
| 421 |
+
assert "--help-all" in out
|
| 422 |
+
return out, err
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def help_all_output_test(subcommand=''):
|
| 426 |
+
"""test that `ipython [subcommand] --help-all` works"""
|
| 427 |
+
cmd = get_ipython_cmd() + [subcommand, '--help-all']
|
| 428 |
+
out, err, rc = get_output_error_code(cmd)
|
| 429 |
+
assert rc == 0, err
|
| 430 |
+
assert "Traceback" not in err
|
| 431 |
+
assert "Options" in out
|
| 432 |
+
assert "Class" in out
|
| 433 |
+
return out, err
|
| 434 |
+
|
lib/python3.12/site-packages/IPython/utils/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (192 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/utils/__pycache__/capture.cpython-312.pyc
ADDED
|
Binary file (8.16 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/utils/__pycache__/decorators.cpython-312.pyc
ADDED
|
Binary file (2.54 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/utils/__pycache__/dir2.cpython-312.pyc
ADDED
|
Binary file (2.56 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/utils/__pycache__/encoding.cpython-312.pyc
ADDED
|
Binary file (2.83 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/utils/__pycache__/frame.cpython-312.pyc
ADDED
|
Binary file (3.8 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/utils/__pycache__/generics.cpython-312.pyc
ADDED
|
Binary file (1.23 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/utils/__pycache__/ipstruct.cpython-312.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|