├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.txt ├── README.rst ├── pyproject.toml ├── src └── ipython_autoimport.py └── tests ├── a ├── __init__.py └── b │ ├── __init__.py │ └── c.py └── test_autoimport.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-20.04 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install 18 | run: | 19 | python -mpip install --upgrade pip setuptools wheel pytest pytest-cov 20 | case '${{ matrix.python-version }}' in 21 | 3.7) 22 | pip install ipython==4.1.0;; 23 | esac 24 | python -mpip install . 25 | - name: Test 26 | run: | 27 | python -mpytest --cov --cov-branch 28 | - name: Upload coverage 29 | uses: actions/upload-artifact@v4 30 | with: 31 | name: coverage-${{ matrix.python-version }} 32 | include-hidden-files: true 33 | path: .coverage 34 | 35 | coverage: 36 | runs-on: ubuntu-latest 37 | needs: build 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: actions/setup-python@v5 41 | with: 42 | python-version: "3.12" 43 | - name: Coverage 44 | run: | 45 | shopt -s globstar && 46 | GH_TOKEN=${{ secrets.GITHUB_TOKEN }} \ 47 | gh run download ${{ github.run-id }} -p 'coverage-*' && 48 | python -mpip install --upgrade coverage && 49 | python -mcoverage combine coverage-*/.coverage && # Unifies paths across envs. 50 | python -mcoverage annotate && 51 | grep -HnTC2 '^!' **/*,cover | sed s/,cover// && 52 | python -mcoverage report --show-missing 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.ipynb_checkpoints/ 2 | *.egg-info/ 3 | .cache/ 4 | .eggs/ 5 | .pytest_cache/ 6 | build/ 7 | dist/ 8 | htmlcov/ 9 | oprofile_data/ 10 | .*.swp 11 | *.o 12 | *.pyc 13 | *.so 14 | .coverage 15 | .gdb_history 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-present Antony Lee 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ipython-autoimport 2 | ================== 3 | 4 | | |GitHub| |PyPI| |Build| 5 | 6 | .. |GitHub| 7 | image:: https://img.shields.io/badge/github-anntzer%2Fdefopt-brightgreen 8 | :target: https://github.com/anntzer/ipython-autoimport 9 | .. |PyPI| 10 | image:: https://img.shields.io/pypi/v/ipython-autoimport.svg?color=brightgreen 11 | :target: https://pypi.python.org/pypi/ipython-autoimport 12 | .. |Build| 13 | image:: https://img.shields.io/github/actions/workflow/status/anntzer/ipython-autoimport/build.yml?branch=main 14 | :target: https://github.com/anntzer/ipython-autoimport/actions 15 | 16 | Automagically import missing modules in IPython: instead of :: 17 | 18 | In [1]: plt.plot([1, 2], [3, 4]) 19 | --------------------------------------------------------------------------- 20 | NameError Traceback (most recent call last) 21 | in () 22 | ----> 1 plt.plot([1, 2], [3, 4]) 23 | 24 | NameError: name 'plt' is not defined 25 | 26 | In [2]: from matplotlib import pyplot as plt 27 | 28 | In [3]: plt.plot([1, 2], [3, 4]) 29 | Out[3]: [] 30 | 31 | do what I mean:: 32 | 33 | In [1]: plt.plot([1, 2], [3, 4]) 34 | Autoimport: from matplotlib import pyplot as plt 35 | Out[1]: [] 36 | 37 | Inspired from @OrangeFlash81's `version 38 | `_, with many 39 | improvements: 40 | 41 | - Does not rely on re-execution, but instead hooks the user namespace; thus, 42 | safe even in the presence of side effects, and works for tab completion and 43 | magics too. 44 | - Learns your preferred aliases from the history -- ``plt`` is not hardcoded to 45 | alias ``matplotlib.pyplot``, just found because you previously imported 46 | ``pyplot`` under this alias. 47 | - Suppresses irrelevant chained tracebacks. 48 | - Auto-imports submodules. 49 | - ``pip``-installable. 50 | 51 | To see auto imports from the current session: ``%autoimport -l`` 52 | 53 | To clear the cache for a symbol with multiple possible imports: ``%autoimport -c SYMBOL`` 54 | 55 | Installation 56 | ------------ 57 | 58 | As usual, install using pip: 59 | 60 | .. code-block:: sh 61 | 62 | $ pip install ipython-autoimport # from PyPI 63 | $ pip install git+https://github.com/anntzer/ipython-autoimport # from Github 64 | 65 | Then, append the output of ``python -m ipython_autoimport`` to the 66 | ``ipython_config.py`` file in the directory printed by ``ipython profile 67 | locate`` (typically ``~/.ipython/profile_default/``). If you don't have such a 68 | file at all, first create it with ``ipython profile create``. 69 | 70 | When using Spyder, the above registration method will not work; instead, add 71 | ``%load_ext ipython_autoimport`` to the 72 | ``Preferences → IPython console → Startup → Run code`` option. 73 | 74 | Note that upon loading, ``ipython_autoimport`` will register its submodule 75 | auto-importer to IPython's "limited evalutation" completer policy (on IPython 76 | versions that support it). 77 | 78 | Run tests with ``pytest``. 79 | 80 | Limitations 81 | ----------- 82 | 83 | Constructs such as :: 84 | 85 | class C: 86 | auto_imported_value 87 | 88 | will not work, because they are run using the class locals (rather than the 89 | patched locals); patching globals would not work because ``LOAD_NAME`` queries 90 | globals using ``PyDict_GetItem`` exactly (note that it queries locals using 91 | ``PyObject_GetItem``; also, ``LOAD_GLOBALS`` queries *both* globals and 92 | builtins using ``PyObject_GetItem`` so we could possibly get away with patching 93 | the builtins dict instead, but that seems a bit too invasive...). 94 | 95 | When using Jedi autocompletion (the default if Jedi is installed as of IPython 96 | 7.2), trying to tab-complete not-yet-imported global names to trigger an import 97 | failure, because Jedi purposefully converts the global dict to a namespace 98 | object and looks up attributes using ``getattr_static``. Jedi can be disabled 99 | by adding ``c.Completer.use_jedi = False`` to the ``ipython_config.py`` file. 100 | 101 | Changelog 102 | --------- 103 | 104 | v0.5.1 (2025-03-11) 105 | ~~~~~~~~~~~~~~~~~~~ 106 | - Fix compatibility with IPython 9's new theme system. 107 | 108 | v0.5 (2024-08-20) 109 | ~~~~~~~~~~~~~~~~~ 110 | - Avoid erroring when exiting IPython≥8.15. 111 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "ipython-autoimport" 7 | description = "Automagically import missing modules in IPython." 8 | readme = "README.rst" 9 | authors = [{name = "Antony Lee"}] 10 | urls = {Repository = "https://github.com/anntzer/ipython-autoimport"} 11 | license = {text = "zlib"} 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Framework :: IPython", 15 | "License :: OSI Approved :: zlib/libpng License", 16 | "Programming Language :: Python :: 3", 17 | ] 18 | requires-python = ">=3.7" 19 | dependencies = [ 20 | "ipython>=4.1", # IPython#8985 is needed for tests to pass(?). 21 | "importlib_metadata; python_version<'3.8'", 22 | ] 23 | dynamic = ["version"] 24 | 25 | [tool.setuptools_scm] 26 | version_scheme = "post-release" 27 | local_scheme = "node-and-date" 28 | fallback_version = "0+unknown" 29 | 30 | [tool.coverage.run] 31 | branch = true 32 | source_pkgs = ["ipython_autoimport"] 33 | 34 | [tool.coverage.paths] 35 | source = ["src/", "/**/python*/site-packages/"] 36 | 37 | [tool.pytest.ini_options] 38 | filterwarnings = [ 39 | "error", 40 | "ignore::DeprecationWarning", 41 | "error::DeprecationWarning:ipython_autoimport", 42 | ] 43 | -------------------------------------------------------------------------------- /src/ipython_autoimport.py: -------------------------------------------------------------------------------- 1 | """ 2 | Automagically import missing modules in IPython. 3 | 4 | To activate, append the output of ``python -m ipython_autoimport`` to the 5 | ``ipython_config.py`` file in the directory printed by ``ipython profile 6 | locate`` (typically ``~/.ipython/profile_default/``). 7 | """ 8 | 9 | import ast 10 | import builtins 11 | import functools 12 | import importlib 13 | import os 14 | import sys 15 | import token 16 | from types import ModuleType 17 | import warnings 18 | 19 | import IPython.core 20 | from IPython.core import magic 21 | from IPython.core.error import UsageError 22 | from IPython.core.magic_arguments import ( 23 | argument, magic_arguments, parse_argstring) 24 | from IPython.core.magics.execution import ExecutionMagics 25 | from IPython.utils import PyColorize 26 | 27 | try: 28 | import importlib.metadata as _im 29 | except ImportError: 30 | import importlib_metadata as _im 31 | try: 32 | __version__ = _im.version("ipython-autoimport") 33 | except (AttributeError, ImportError): # AttrError if i_m is missing. 34 | __version__ = "(unknown version)" 35 | 36 | 37 | def _get_import_cache(ipython): 38 | """ 39 | Load a mapping of names to import statements from the IPython history. 40 | """ 41 | 42 | import_cache = {} 43 | 44 | def _format_alias(alias): 45 | return ("import {0.name} as {0.asname}" if alias.asname 46 | else "import {0.name}").format(alias) 47 | 48 | class Visitor(ast.NodeVisitor): 49 | def visit_Import(self, node): 50 | for alias in node.names: 51 | (import_cache.setdefault(alias.asname or alias.name, set()) 52 | .add(_format_alias(alias))) 53 | 54 | def visit_ImportFrom(self, node): 55 | if node.level: # Skip relative imports. 56 | return 57 | for alias in node.names: 58 | (import_cache.setdefault(alias.asname or alias.name, set()) 59 | .add("from {} {}".format(node.module, _format_alias(alias)))) 60 | 61 | for _, _, entry in ( 62 | ipython.history_manager.get_tail( 63 | ipython.history_load_length, raw=True)): 64 | if entry.startswith("%autoimport"): 65 | try: 66 | args = parse_argstring( 67 | AutoImportMagics.autoimport, entry[len("%autoimport"):]) 68 | if args.clear: 69 | import_cache.pop(args.clear, None) 70 | except UsageError: 71 | pass 72 | else: 73 | try: 74 | with warnings.catch_warnings(): 75 | warnings.simplefilter("ignore", SyntaxWarning) 76 | parsed = ast.parse(entry) 77 | except SyntaxError: 78 | continue 79 | Visitor().visit(parsed) 80 | 81 | return import_cache 82 | 83 | 84 | def _report(ipython, msg): 85 | """Output a message prepended by a colored `Autoimport:` tag.""" 86 | # Tell prompt_toolkit to pass ANSI escapes through (PTK#187); harmless on 87 | # pre-PTK versions. 88 | try: 89 | sys.stdout._raw = True 90 | except AttributeError: 91 | pass 92 | if IPython.version_info >= (9,): 93 | import pygments # Only a dependency from IPython 4.2.0. 94 | print(PyColorize.theme_table[ipython.colors].format([ 95 | (pygments.token.Number, "Autoimport: "), 96 | (pygments.token.Text, msg), 97 | ])) 98 | else: 99 | cs = PyColorize.Parser().color_table[ipython.colors].colors 100 | # Token.NUMBER: bright blue (cyan), looks reasonable. 101 | print("{}Autoimport:{} {}".format(cs[token.NUMBER], cs["normal"], msg)) 102 | 103 | 104 | class _SubmoduleAutoImporterModule(ModuleType): 105 | # __module and __ipython are set externally to not modify the constructor. 106 | 107 | @property 108 | def __dict__(self): 109 | return self.__module.__dict__ 110 | 111 | # Overriding __setattr__ is needed even when __dict__ is overridden. 112 | def __setattr__(self, name, value): 113 | setattr(self.__module, name, value) 114 | 115 | def __getattr__(self, name): 116 | try: 117 | value = getattr(self.__module, name) 118 | if isinstance(value, ModuleType): 119 | value = _make_submodule_autoimporter_module( 120 | self.__ipython, value) 121 | return value 122 | except AttributeError: 123 | import_target = "{}.{}".format(self.__name__, name) 124 | try: 125 | submodule = importlib.import_module(import_target) 126 | except getattr(builtins, "ModuleNotFoundError", ImportError): 127 | pass # Py<3.6. 128 | else: 129 | _report(self.__ipython, "import {}".format(import_target)) 130 | return _make_submodule_autoimporter_module( 131 | self.__ipython, submodule) 132 | raise # Raise AttributeError without chaining ImportError. 133 | 134 | 135 | def _make_submodule_autoimporter_module(ipython, module): 136 | """Return a module sub-instance that automatically imports submodules.""" 137 | if not hasattr(module, "__path__"): # We only need to wrap packages. 138 | return module 139 | saim = _SubmoduleAutoImporterModule(module.__name__) 140 | for k, v in [ 141 | ("_SubmoduleAutoImporterModule__module", module), 142 | ("_SubmoduleAutoImporterModule__ipython", ipython), 143 | # Apparently, `module?` does not trigger descriptors, so we need to 144 | # set the docstring explicitly (on the instance, not on the class). 145 | # Then then only difference in the output of `module?` becomes the 146 | # type (`SubmoduleAutoImportModule` instead of `module`), which we 147 | # should keep for clarity. 148 | ("__doc__", module.__doc__), 149 | ]: 150 | ModuleType.__setattr__(saim, k, v) 151 | return saim 152 | 153 | 154 | class _AutoImporterMap(dict): 155 | """Mapping that attempts to resolve missing keys through imports.""" 156 | 157 | def __init__(self, ipython): 158 | super().__init__(ipython.user_ns) 159 | self._ipython = ipython 160 | self._import_cache = _get_import_cache(ipython) 161 | self._imported = [] 162 | 163 | def __getitem__(self, name): 164 | try: 165 | value = super().__getitem__(name) 166 | except KeyError as key_error: 167 | # First try to resolve through builtins, so that local directories 168 | # (e.g., "bin") do not override them (by being misinterpreted as 169 | # a namespace package). In this case, we do not need to check 170 | # whether we got a module. 171 | try: 172 | return getattr(builtins, name) 173 | except AttributeError: 174 | pass 175 | # Find single matching import, if any. 176 | imports = self._import_cache.get(name, {"import {}".format(name)}) 177 | if len(imports) != 1: 178 | if len(imports) > 1: 179 | _report(self._ipython, 180 | "multiple imports available for {!r}:\n" 181 | "{}\n" 182 | "'%autoimport --clear {}' " 183 | "can be used to clear the cache for this symbol." 184 | .format(name, "\n".join(imports), name)) 185 | raise key_error 186 | import_source, = imports 187 | try: 188 | exec(import_source, self) # exec recasts self as a dict. 189 | except Exception: # Normally, ImportError. 190 | raise key_error 191 | else: 192 | self._imported.append(import_source) 193 | _report(self._ipython, import_source) 194 | value = super().__getitem__(name) 195 | if isinstance(value, ModuleType): 196 | return _make_submodule_autoimporter_module(self._ipython, value) 197 | else: 198 | return value 199 | 200 | # Ensure that closures that attempt to resolve into globals get the right 201 | # values. 202 | 203 | def __setitem__(self, name, value): 204 | super().__setitem__(name, value) 205 | setattr(self._ipython.user_module, name, value) 206 | 207 | def __delitem__(self, name): 208 | super().__delitem__(name) 209 | try: 210 | delattr(self._ipython.user_module, name) 211 | except AttributeError: 212 | raise KeyError(name) 213 | 214 | 215 | def _patch_magic(func): 216 | @functools.wraps(func) 217 | def magic(self, *args, **kwargs): 218 | _uninstall_namespace(self.shell) 219 | try: 220 | return func(self, *args, **kwargs) 221 | finally: 222 | _install_namespace(self.shell) 223 | 224 | return magic 225 | 226 | 227 | @magic.magics_class 228 | class _PatchedMagics(ExecutionMagics): 229 | time = magic.line_cell_magic(_patch_magic(ExecutionMagics.time)) 230 | timeit = magic.line_cell_magic(_patch_magic(ExecutionMagics.timeit)) 231 | prun = magic.line_cell_magic(_patch_magic(ExecutionMagics.prun)) 232 | 233 | 234 | @magic.magics_class 235 | class _UnpatchedMagics(ExecutionMagics): 236 | time = magic.line_cell_magic(ExecutionMagics.time) 237 | timeit = magic.line_cell_magic(ExecutionMagics.timeit) 238 | prun = magic.line_cell_magic(ExecutionMagics.prun) 239 | 240 | 241 | def _install_namespace(ipython): 242 | # `Completer.namespace` needs to be overriden too, for completion to work 243 | # (both with and without Jedi). 244 | ipython.user_ns = ipython.Completer.namespace = ( 245 | _AutoImporterMap(ipython)) 246 | if hasattr(IPython.core, "guarded_eval"): 247 | (IPython.core.guarded_eval.EVALUATION_POLICIES["limited"] 248 | .allowed_getattr.add(_SubmoduleAutoImporterModule)) 249 | 250 | 251 | def _uninstall_namespace(ipython): 252 | ipython.user_ns = ipython.Completer.namespace = dict(ipython.user_ns) 253 | 254 | 255 | @magic.magics_class 256 | class AutoImportMagics(magic.Magics): 257 | @magic.line_magic 258 | @magic_arguments() 259 | @argument("-c", "--clear", type=str, help="Clear cache for this symbol") 260 | @argument("-l", "--list", dest="list", action="store_const", 261 | const=True, default=False, 262 | help="Show autoimports from this session") 263 | def autoimport(self, arg): 264 | args = parse_argstring(AutoImportMagics.autoimport, arg) 265 | 266 | if args.clear: 267 | if self.shell.user_ns._import_cache.pop(args.clear, None): 268 | _report( 269 | self.shell, 270 | f"cleared symbol {args.clear!r} from autoimport cache.") 271 | else: 272 | _report( 273 | self.shell, 274 | f"didn't find symbol {args.clear!r} in autoimport cache.") 275 | 276 | if args.list: 277 | if self.shell.user_ns._imported: 278 | _report(self.shell, 279 | "the following autoimports were run:\n{}".format( 280 | "\n".join(self.shell.user_ns._imported))) 281 | else: 282 | _report(self.shell, "no autoimports in this session yet.") 283 | 284 | 285 | def load_ipython_extension(ipython): 286 | _install_namespace(ipython) 287 | ipython.register_magics(_PatchedMagics) # Add warning to timing magics. 288 | ipython.register_magics(AutoImportMagics) 289 | 290 | 291 | def unload_ipython_extension(ipython): 292 | _uninstall_namespace(ipython) 293 | ipython.register_magics(_UnpatchedMagics) # Unpatch timing magics. 294 | 295 | 296 | if __name__ == "__main__": 297 | if os.isatty(sys.stdout.fileno()): 298 | print("""\ 299 | # Please append the output of this command to the config file in 300 | # the directory specified by `ipython profile locate` (typically 301 | # `~/.ipython/profile_default/ipython_config.py`) 302 | """) 303 | print("""\ 304 | c.InteractiveShellApp.exec_lines.append( 305 | "try:\\n %load_ext ipython_autoimport\\nexcept ImportError: pass")""") 306 | -------------------------------------------------------------------------------- /tests/a/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anntzer/ipython-autoimport/c780af39fbbba87a24bf04327adfdbbe6f3eb29e/tests/a/__init__.py -------------------------------------------------------------------------------- /tests/a/b/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anntzer/ipython-autoimport/c780af39fbbba87a24bf04327adfdbbe6f3eb29e/tests/a/b/__init__.py -------------------------------------------------------------------------------- /tests/a/b/c.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anntzer/ipython-autoimport/c780af39fbbba87a24bf04327adfdbbe6f3eb29e/tests/a/b/c.py -------------------------------------------------------------------------------- /tests/test_autoimport.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import tokenize 3 | 4 | import IPython.utils.io 5 | import IPython.testing.globalipapp 6 | import ipython_autoimport 7 | import pytest 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def global_ip(): 12 | ip = IPython.testing.globalipapp.start_ipython() 13 | path = Path(__file__) 14 | ip.run_cell("import sys; sys.path[:0] = [{!r}, {!r}]".format( 15 | str(path.parents[1]), str(path.parents[0]))) 16 | return ip 17 | 18 | 19 | @pytest.fixture 20 | def ip(global_ip): 21 | global_ip.run_cell("%reset -f") 22 | global_ip.run_cell("%load_ext ipython_autoimport") 23 | yield global_ip 24 | with IPython.utils.io.capture_output(): 25 | global_ip.run_cell( 26 | "for name, mod in list(sys.modules.items()):\n" 27 | " if getattr(mod, '__file__', '').startswith({!r}):\n" 28 | " del sys.modules[name]" 29 | .format(str(Path(__file__).parent))) 30 | global_ip.run_cell("%unload_ext ipython_autoimport") 31 | 32 | 33 | @pytest.mark.parametrize("name", ["a", "a.b", "a.b.c"]) 34 | def test_autoimport(ip, name): 35 | with IPython.utils.io.capture_output() as captured: 36 | ip.run_cell("{}.__name__".format(name)) 37 | parts = name.split(".") 38 | assert (captured.stdout 39 | == "{}Out[1]: {!r}\n".format( 40 | "".join("Autoimport: import {}\n".format( 41 | ".".join(parts[:i + 1])) for i in range(len(parts))), 42 | name)) 43 | 44 | 45 | def test_sub_submodule(ip): 46 | ip.run_cell("import a.b") 47 | with IPython.utils.io.capture_output() as captured: 48 | ip.run_cell("a.b.c.__name__") 49 | assert captured.stdout == "Autoimport: import a.b.c\nOut[1]: 'a.b.c'\n" 50 | 51 | 52 | def test_no_import(ip): 53 | with IPython.utils.io.capture_output() as captured: 54 | ip.run_cell("a.not_here") 55 | # Exact message changes between Python versions. 56 | assert "has no attribute 'not_here'" in captured.stdout.splitlines()[-1] 57 | assert "ImportError" not in captured.stdout 58 | 59 | 60 | def test_setattr(ip): 61 | with IPython.utils.io.capture_output() as captured: 62 | ip.run_cell("a; a.b = 42; 'b' in vars(a), a.b") 63 | assert captured.stdout == "Autoimport: import a\nOut[1]: (True, 42)\n" 64 | 65 | 66 | def test_closure(ip): 67 | with IPython.utils.io.capture_output() as captured: 68 | ip.run_cell("x = 1; (lambda: x)()") 69 | assert captured.stdout == "Out[1]: 1\n" 70 | 71 | 72 | def test_del(ip): 73 | with IPython.utils.io.capture_output() as captured: 74 | ip.run_cell("x = 1; del x; print('ok')") 75 | assert captured.stdout == "ok\n" 76 | 77 | 78 | def test_list(ip): 79 | ip.run_cell("os") 80 | with IPython.utils.io.capture_output() as captured: 81 | ip.run_cell("%autoimport -l") 82 | assert (captured.stdout == 83 | "Autoimport: the following autoimports were run:\nimport os\n") 84 | 85 | 86 | def test_no_list(ip): 87 | with IPython.utils.io.capture_output() as captured: 88 | ip.run_cell("%autoimport -l") 89 | assert (captured.stdout == 90 | "Autoimport: no autoimports in this session yet.\n") 91 | 92 | 93 | def test_noclear(ip): 94 | with IPython.utils.io.capture_output() as captured: 95 | ip.run_cell("%autoimport -c ipython_autoimport_test_noclear") 96 | assert ( 97 | captured.stdout == 98 | "Autoimport: didn't find symbol " 99 | "'ipython_autoimport_test_noclear' in autoimport cache.\n" 100 | ) 101 | 102 | 103 | @pytest.mark.parametrize("magic", ["time", "timeit -n 1 -r 1", "prun"]) 104 | def test_magics(ip, magic): 105 | with IPython.utils.io.capture_output() as captured: 106 | ip.run_cell("{} x = 1".format(magic)) 107 | assert "error" not in captured.stdout.lower() 108 | 109 | 110 | def test_no_autoimport_in_time(ip): 111 | with IPython.utils.io.capture_output() as captured: 112 | ip.run_cell("%time type(get_ipython().user_ns)") 113 | assert "autoimport" not in captured.stdout.lower() 114 | 115 | 116 | def test_unload(ip): 117 | with IPython.utils.io.capture_output() as captured: 118 | ip.run_cell("%unload_ext ipython_autoimport") 119 | ip.run_cell("try: a\nexcept NameError: print('ok')") 120 | assert captured.stdout == "ok\n" 121 | 122 | 123 | class TestStyle: 124 | def _iter_stripped_lines(self): 125 | for path in [ipython_autoimport.__file__, __file__]: 126 | with tokenize.open(path) as src: 127 | for i, line in enumerate(src, 1): 128 | yield "{}:{}".format(path, i), line.rstrip("\n") 129 | 130 | def test_line_length(self): 131 | for name, line in self._iter_stripped_lines(): 132 | assert len(line) <= 79, f"{name} is too long" 133 | 134 | def test_trailing_whitespace(self): 135 | for name, line in self._iter_stripped_lines(): 136 | assert not (line and line[-1].isspace()), \ 137 | f"{name} has trailing whitespace" 138 | --------------------------------------------------------------------------------