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/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/__pycache__/__main__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/__pycache__/display.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/__pycache__/paths.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/extensions/__init__.py +2 -0
- lib/python3.12/site-packages/IPython/extensions/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/extensions/__pycache__/autoreload.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/extensions/__pycache__/storemagic.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/extensions/autoreload.py +915 -0
- lib/python3.12/site-packages/IPython/extensions/deduperreload/__init__.py +0 -0
- lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload_patching.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload.py +637 -0
- lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload_patching.py +142 -0
- lib/python3.12/site-packages/IPython/extensions/storemagic.py +236 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/debugger.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/embed.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/interactiveshell.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/ipapp.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/magics.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/prompts.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/__pycache__/ptutils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_match.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_suggest.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/filters.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/IPython/terminal/shortcuts/auto_suggest.py +658 -0
- lib/python3.12/site-packages/attrs/__init__.py +72 -0
- lib/python3.12/site-packages/attrs/__init__.pyi +314 -0
- lib/python3.12/site-packages/attrs/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/attrs/__pycache__/converters.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/attrs/__pycache__/exceptions.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/attrs/__pycache__/filters.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/attrs/__pycache__/setters.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/attrs/__pycache__/validators.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/attrs/converters.py +3 -0
- lib/python3.12/site-packages/attrs/exceptions.py +3 -0
- lib/python3.12/site-packages/attrs/filters.py +3 -0
- lib/python3.12/site-packages/attrs/py.typed +0 -0
- lib/python3.12/site-packages/attrs/setters.py +3 -0
- lib/python3.12/site-packages/attrs/validators.py +3 -0
- lib/python3.12/site-packages/torchaudio/__init__.py +53 -0
- lib/python3.12/site-packages/torchaudio/_internal/__init__.py +10 -0
- lib/python3.12/site-packages/torchaudio/_internal/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torchaudio/_internal/__pycache__/module_utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/torchaudio/_internal/module_utils.py +113 -0
- lib/python3.12/site-packages/torchaudio/compliance/__init__.py +5 -0
- lib/python3.12/site-packages/torchaudio/compliance/__pycache__/__init__.cpython-312.pyc +0 -0
lib/python3.12/site-packages/IPython/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (4.97 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/__pycache__/__main__.cpython-312.pyc
ADDED
|
Binary file (310 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/__pycache__/display.cpython-312.pyc
ADDED
|
Binary file (896 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/__pycache__/paths.cpython-312.pyc
ADDED
|
Binary file (5.54 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/extensions/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""This directory is meant for IPython extensions."""
|
lib/python3.12/site-packages/IPython/extensions/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (259 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/extensions/__pycache__/autoreload.cpython-312.pyc
ADDED
|
Binary file (33.9 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/extensions/__pycache__/storemagic.cpython-312.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/extensions/autoreload.py
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IPython extension to reload modules before executing user code.
|
| 2 |
+
|
| 3 |
+
``autoreload`` reloads modules automatically before entering the execution of
|
| 4 |
+
code typed at the IPython prompt.
|
| 5 |
+
|
| 6 |
+
This makes for example the following workflow possible:
|
| 7 |
+
|
| 8 |
+
.. sourcecode:: ipython
|
| 9 |
+
|
| 10 |
+
In [1]: %load_ext autoreload
|
| 11 |
+
|
| 12 |
+
In [2]: %autoreload 2
|
| 13 |
+
|
| 14 |
+
In [3]: from foo import some_function
|
| 15 |
+
|
| 16 |
+
In [4]: some_function()
|
| 17 |
+
Out[4]: 42
|
| 18 |
+
|
| 19 |
+
In [5]: # open foo.py in an editor and change some_function to return 43
|
| 20 |
+
|
| 21 |
+
In [6]: some_function()
|
| 22 |
+
Out[6]: 43
|
| 23 |
+
|
| 24 |
+
The module was reloaded without reloading it explicitly, and the object
|
| 25 |
+
imported with ``from foo import ...`` was also updated.
|
| 26 |
+
|
| 27 |
+
Usage
|
| 28 |
+
=====
|
| 29 |
+
|
| 30 |
+
The following magic commands are provided:
|
| 31 |
+
|
| 32 |
+
``%autoreload``, ``%autoreload now``
|
| 33 |
+
|
| 34 |
+
Reload all modules (except those excluded by ``%aimport``)
|
| 35 |
+
automatically now.
|
| 36 |
+
|
| 37 |
+
``%autoreload 0``, ``%autoreload off``
|
| 38 |
+
|
| 39 |
+
Disable automatic reloading.
|
| 40 |
+
|
| 41 |
+
``%autoreload 1``, ``%autoreload explicit``
|
| 42 |
+
|
| 43 |
+
Reload all modules imported with ``%aimport`` every time before
|
| 44 |
+
executing the Python code typed.
|
| 45 |
+
|
| 46 |
+
``%autoreload 2``, ``%autoreload all``
|
| 47 |
+
|
| 48 |
+
Reload all modules (except those excluded by ``%aimport``) every
|
| 49 |
+
time before executing the Python code typed.
|
| 50 |
+
|
| 51 |
+
``%autoreload 3``, ``%autoreload complete``
|
| 52 |
+
|
| 53 |
+
Same as 2/all, but also adds any new objects in the module. See
|
| 54 |
+
unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects
|
| 55 |
+
|
| 56 |
+
Adding ``--print`` or ``-p`` to the ``%autoreload`` line will print autoreload activity to
|
| 57 |
+
standard out. ``--log`` or ``-l`` will do it to the log at INFO level; both can be used
|
| 58 |
+
simultaneously.
|
| 59 |
+
|
| 60 |
+
``%aimport``
|
| 61 |
+
|
| 62 |
+
List modules which are to be automatically imported or not to be imported.
|
| 63 |
+
|
| 64 |
+
``%aimport foo``
|
| 65 |
+
|
| 66 |
+
Import module 'foo' and mark it to be autoreloaded for ``%autoreload 1``
|
| 67 |
+
|
| 68 |
+
``%aimport foo, bar``
|
| 69 |
+
|
| 70 |
+
Import modules 'foo', 'bar' and mark them to be autoreloaded for ``%autoreload 1``
|
| 71 |
+
|
| 72 |
+
``%aimport -foo``
|
| 73 |
+
|
| 74 |
+
Mark module 'foo' to not be autoreloaded.
|
| 75 |
+
|
| 76 |
+
Import Conflict Resolution
|
| 77 |
+
==========================
|
| 78 |
+
|
| 79 |
+
In ``%autoreload 3`` mode, the extension tracks ``from X import Y`` style imports
|
| 80 |
+
and intelligently resolves conflicts when the same name is imported multiple ways.
|
| 81 |
+
|
| 82 |
+
Import tracking occurs after successful code execution, ensuring that only valid
|
| 83 |
+
imports are tracked. This approach handles edge cases such as:
|
| 84 |
+
|
| 85 |
+
- Importing a name that doesn't initially exist in a module, then adding that name
|
| 86 |
+
to the module and importing it again
|
| 87 |
+
- Conflicts between aliased imports (``from X import Y as Z``) and direct imports
|
| 88 |
+
(``from X import Z``)
|
| 89 |
+
|
| 90 |
+
When conflicts occur:
|
| 91 |
+
|
| 92 |
+
- If you first do ``from X import Y as Z`` then later ``from X import Z``,
|
| 93 |
+
the extension will switch to reloading ``Z`` instead of ``Y`` under the name ``Z``.
|
| 94 |
+
|
| 95 |
+
- Similarly, if you first do ``from X import Z`` then later ``from X import Y as Z``,
|
| 96 |
+
the extension will switch to reloading ``Y`` as ``Z`` instead of the original ``Z``.
|
| 97 |
+
|
| 98 |
+
- The most recent successful import always takes precedence in conflict resolution.
|
| 99 |
+
|
| 100 |
+
Caveats
|
| 101 |
+
=======
|
| 102 |
+
|
| 103 |
+
Reloading Python modules in a reliable way is in general difficult,
|
| 104 |
+
and unexpected things may occur. ``%autoreload`` tries to work around
|
| 105 |
+
common pitfalls by replacing function code objects and parts of
|
| 106 |
+
classes previously in the module with new versions. This makes the
|
| 107 |
+
following things to work:
|
| 108 |
+
|
| 109 |
+
- Functions and classes imported via 'from xxx import foo' are upgraded
|
| 110 |
+
to new versions when 'xxx' is reloaded.
|
| 111 |
+
|
| 112 |
+
- Methods and properties of classes are upgraded on reload, so that
|
| 113 |
+
calling 'c.foo()' on an object 'c' created before the reload causes
|
| 114 |
+
the new code for 'foo' to be executed.
|
| 115 |
+
|
| 116 |
+
Some of the known remaining caveats are:
|
| 117 |
+
|
| 118 |
+
- Replacing code objects does not always succeed: changing a @property
|
| 119 |
+
in a class to an ordinary method or a method to a member variable
|
| 120 |
+
can cause problems (but in old objects only).
|
| 121 |
+
|
| 122 |
+
- Functions that are removed (eg. via monkey-patching) from a module
|
| 123 |
+
before it is reloaded are not upgraded.
|
| 124 |
+
|
| 125 |
+
- C extension modules cannot be reloaded, and so cannot be autoreloaded.
|
| 126 |
+
|
| 127 |
+
- While comparing Enum and Flag, the 'is' Identity Operator is used (even in the case '==' has been used (Similar to the 'None' keyword)).
|
| 128 |
+
|
| 129 |
+
- Reloading a module, or importing the same module by a different name, creates new Enums. These may look the same, but are not.
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
from IPython.core import magic_arguments
|
| 133 |
+
from IPython.core.magic import Magics, magics_class, line_magic
|
| 134 |
+
from IPython.extensions.deduperreload.deduperreload import DeduperReloader
|
| 135 |
+
|
| 136 |
+
__skip_doctest__ = True
|
| 137 |
+
|
| 138 |
+
# -----------------------------------------------------------------------------
|
| 139 |
+
# Copyright (C) 2000 Thomas Heller
|
| 140 |
+
# Copyright (C) 2008 Pauli Virtanen <pav@iki.fi>
|
| 141 |
+
# Copyright (C) 2012 The IPython Development Team
|
| 142 |
+
#
|
| 143 |
+
# Distributed under the terms of the BSD License. The full license is in
|
| 144 |
+
# the file COPYING, distributed as part of this software.
|
| 145 |
+
# -----------------------------------------------------------------------------
|
| 146 |
+
#
|
| 147 |
+
# This IPython module is written by Pauli Virtanen, based on the autoreload
|
| 148 |
+
# code by Thomas Heller.
|
| 149 |
+
|
| 150 |
+
# -----------------------------------------------------------------------------
|
| 151 |
+
# Imports
|
| 152 |
+
# -----------------------------------------------------------------------------
|
| 153 |
+
|
| 154 |
+
import ast
|
| 155 |
+
import os
|
| 156 |
+
import sys
|
| 157 |
+
import traceback
|
| 158 |
+
import types
|
| 159 |
+
import weakref
|
| 160 |
+
import gc
|
| 161 |
+
import logging
|
| 162 |
+
from importlib import import_module, reload
|
| 163 |
+
from importlib.util import source_from_cache
|
| 164 |
+
|
| 165 |
+
# ------------------------------------------------------------------------------
|
| 166 |
+
# Autoreload functionality
|
| 167 |
+
# ------------------------------------------------------------------------------
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
class ModuleReloader:
|
| 171 |
+
enabled = False
|
| 172 |
+
"""Whether this reloader is enabled"""
|
| 173 |
+
|
| 174 |
+
check_all = True
|
| 175 |
+
"""Autoreload all modules, not just those listed in 'modules'"""
|
| 176 |
+
|
| 177 |
+
autoload_obj = False
|
| 178 |
+
"""Autoreload all modules AND autoload all new objects"""
|
| 179 |
+
|
| 180 |
+
def __init__(self, shell=None):
|
| 181 |
+
# Modules that failed to reload: {module: mtime-on-failed-reload, ...}
|
| 182 |
+
self.failed = {}
|
| 183 |
+
# Modules specially marked as autoreloadable.
|
| 184 |
+
self.modules = {}
|
| 185 |
+
# Modules specially marked as not autoreloadable.
|
| 186 |
+
self.skip_modules = {}
|
| 187 |
+
# (module-name, name) -> weakref, for replacing old code objects
|
| 188 |
+
self.old_objects = {}
|
| 189 |
+
# Module modification timestamps
|
| 190 |
+
self.modules_mtimes = {}
|
| 191 |
+
self.shell = shell
|
| 192 |
+
|
| 193 |
+
# Reporting callable for verbosity
|
| 194 |
+
self._report = lambda msg: None # by default, be quiet.
|
| 195 |
+
|
| 196 |
+
# Deduper reloader
|
| 197 |
+
self.deduper_reloader = DeduperReloader()
|
| 198 |
+
|
| 199 |
+
# Persistent import tracker for from-imports
|
| 200 |
+
self.import_from_tracker = ImportFromTracker({}, {})
|
| 201 |
+
|
| 202 |
+
# Cache module modification times
|
| 203 |
+
self.check(check_all=True, do_reload=False)
|
| 204 |
+
|
| 205 |
+
# To hide autoreload errors
|
| 206 |
+
self.hide_errors = False
|
| 207 |
+
|
| 208 |
+
def mark_module_skipped(self, module_name):
|
| 209 |
+
"""Skip reloading the named module in the future"""
|
| 210 |
+
try:
|
| 211 |
+
del self.modules[module_name]
|
| 212 |
+
except KeyError:
|
| 213 |
+
pass
|
| 214 |
+
self.skip_modules[module_name] = True
|
| 215 |
+
|
| 216 |
+
def mark_module_reloadable(self, module_name):
|
| 217 |
+
"""Reload the named module in the future (if it is imported)"""
|
| 218 |
+
try:
|
| 219 |
+
del self.skip_modules[module_name]
|
| 220 |
+
except KeyError:
|
| 221 |
+
pass
|
| 222 |
+
self.modules[module_name] = True
|
| 223 |
+
|
| 224 |
+
def clear_import_tracker(self):
|
| 225 |
+
"""Clear the persistent import tracker state"""
|
| 226 |
+
self.import_from_tracker = ImportFromTracker({}, {})
|
| 227 |
+
|
| 228 |
+
def aimport_module(self, module_name):
|
| 229 |
+
"""Import a module, and mark it reloadable
|
| 230 |
+
|
| 231 |
+
Returns
|
| 232 |
+
-------
|
| 233 |
+
top_module : module
|
| 234 |
+
The imported module if it is top-level, or the top-level
|
| 235 |
+
top_name : module
|
| 236 |
+
Name of top_module
|
| 237 |
+
|
| 238 |
+
"""
|
| 239 |
+
self.mark_module_reloadable(module_name)
|
| 240 |
+
|
| 241 |
+
import_module(module_name)
|
| 242 |
+
top_name = module_name.split(".")[0]
|
| 243 |
+
top_module = sys.modules[top_name]
|
| 244 |
+
return top_module, top_name
|
| 245 |
+
|
| 246 |
+
def filename_and_mtime(self, module):
|
| 247 |
+
if not hasattr(module, "__file__") or module.__file__ is None:
|
| 248 |
+
return None, None
|
| 249 |
+
|
| 250 |
+
if getattr(module, "__name__", None) in [None, "__mp_main__", "__main__"]:
|
| 251 |
+
# we cannot reload(__main__) or reload(__mp_main__)
|
| 252 |
+
return None, None
|
| 253 |
+
|
| 254 |
+
filename = module.__file__
|
| 255 |
+
path, ext = os.path.splitext(filename)
|
| 256 |
+
|
| 257 |
+
if ext.lower() == ".py":
|
| 258 |
+
py_filename = filename
|
| 259 |
+
else:
|
| 260 |
+
try:
|
| 261 |
+
py_filename = source_from_cache(filename)
|
| 262 |
+
except ValueError:
|
| 263 |
+
return None, None
|
| 264 |
+
|
| 265 |
+
try:
|
| 266 |
+
pymtime = os.stat(py_filename).st_mtime
|
| 267 |
+
except OSError:
|
| 268 |
+
return None, None
|
| 269 |
+
|
| 270 |
+
return py_filename, pymtime
|
| 271 |
+
|
| 272 |
+
def check(self, check_all=False, do_reload=True, execution_info=None):
|
| 273 |
+
"""Check whether some modules need to be reloaded."""
|
| 274 |
+
|
| 275 |
+
if not self.enabled and not check_all:
|
| 276 |
+
return
|
| 277 |
+
|
| 278 |
+
if check_all or self.check_all:
|
| 279 |
+
modules = list(sys.modules.keys())
|
| 280 |
+
else:
|
| 281 |
+
modules = list(self.modules.keys())
|
| 282 |
+
|
| 283 |
+
# Use the persistent import_from_tracker
|
| 284 |
+
import_from_tracker = (
|
| 285 |
+
self.import_from_tracker if self.import_from_tracker.imports_froms else None
|
| 286 |
+
)
|
| 287 |
+
for modname in modules:
|
| 288 |
+
m = sys.modules.get(modname, None)
|
| 289 |
+
|
| 290 |
+
if modname in self.skip_modules:
|
| 291 |
+
continue
|
| 292 |
+
|
| 293 |
+
py_filename, pymtime = self.filename_and_mtime(m)
|
| 294 |
+
if py_filename is None:
|
| 295 |
+
continue
|
| 296 |
+
|
| 297 |
+
try:
|
| 298 |
+
if pymtime <= self.modules_mtimes[modname]:
|
| 299 |
+
continue
|
| 300 |
+
except KeyError:
|
| 301 |
+
self.modules_mtimes[modname] = pymtime
|
| 302 |
+
continue
|
| 303 |
+
else:
|
| 304 |
+
if self.failed.get(py_filename, None) == pymtime:
|
| 305 |
+
continue
|
| 306 |
+
|
| 307 |
+
self.modules_mtimes[modname] = pymtime
|
| 308 |
+
|
| 309 |
+
# If we've reached this point, we should try to reload the module
|
| 310 |
+
if do_reload:
|
| 311 |
+
self._report(f"Reloading '{modname}'.")
|
| 312 |
+
try:
|
| 313 |
+
if self.autoload_obj:
|
| 314 |
+
superreload(
|
| 315 |
+
m,
|
| 316 |
+
reload,
|
| 317 |
+
self.old_objects,
|
| 318 |
+
self.shell,
|
| 319 |
+
import_from_tracker=import_from_tracker,
|
| 320 |
+
)
|
| 321 |
+
# if not using autoload, check if deduperreload is viable for this module
|
| 322 |
+
elif self.deduper_reloader.maybe_reload_module(m):
|
| 323 |
+
pass
|
| 324 |
+
else:
|
| 325 |
+
superreload(m, reload, self.old_objects)
|
| 326 |
+
if py_filename in self.failed:
|
| 327 |
+
del self.failed[py_filename]
|
| 328 |
+
except:
|
| 329 |
+
if not self.hide_errors:
|
| 330 |
+
logger = logging.getLogger("autoreload")
|
| 331 |
+
logger.exception(
|
| 332 |
+
f"Failed to reload module '{modname}' from file '{py_filename}'"
|
| 333 |
+
)
|
| 334 |
+
print(
|
| 335 |
+
"[autoreload of {} failed: {}]".format(
|
| 336 |
+
modname, traceback.format_exc(10)
|
| 337 |
+
),
|
| 338 |
+
file=sys.stderr,
|
| 339 |
+
)
|
| 340 |
+
self.failed[py_filename] = pymtime
|
| 341 |
+
self.deduper_reloader.update_sources()
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
# ------------------------------------------------------------------------------
|
| 345 |
+
# superreload
|
| 346 |
+
# ------------------------------------------------------------------------------
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
func_attrs = [
|
| 350 |
+
"__code__",
|
| 351 |
+
"__defaults__",
|
| 352 |
+
"__doc__",
|
| 353 |
+
"__closure__",
|
| 354 |
+
"__globals__",
|
| 355 |
+
"__dict__",
|
| 356 |
+
]
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def update_function(old, new):
|
| 360 |
+
"""Upgrade the code object of a function"""
|
| 361 |
+
for name in func_attrs:
|
| 362 |
+
try:
|
| 363 |
+
setattr(old, name, getattr(new, name))
|
| 364 |
+
except (AttributeError, TypeError):
|
| 365 |
+
pass
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def update_instances(old, new):
|
| 369 |
+
"""Use garbage collector to find all instances that refer to the old
|
| 370 |
+
class definition and update their __class__ to point to the new class
|
| 371 |
+
definition"""
|
| 372 |
+
|
| 373 |
+
refs = gc.get_referrers(old)
|
| 374 |
+
|
| 375 |
+
for ref in refs:
|
| 376 |
+
if type(ref) is old:
|
| 377 |
+
object.__setattr__(ref, "__class__", new)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def update_class(old, new):
|
| 381 |
+
"""Replace stuff in the __dict__ of a class, and upgrade
|
| 382 |
+
method code objects, and add new methods, if any"""
|
| 383 |
+
for key in list(old.__dict__.keys()):
|
| 384 |
+
old_obj = getattr(old, key)
|
| 385 |
+
try:
|
| 386 |
+
new_obj = getattr(new, key)
|
| 387 |
+
# explicitly checking that comparison returns True to handle
|
| 388 |
+
# cases where `==` doesn't return a boolean.
|
| 389 |
+
if (old_obj == new_obj) is True:
|
| 390 |
+
continue
|
| 391 |
+
except AttributeError:
|
| 392 |
+
# obsolete attribute: remove it
|
| 393 |
+
try:
|
| 394 |
+
delattr(old, key)
|
| 395 |
+
except (AttributeError, TypeError):
|
| 396 |
+
pass
|
| 397 |
+
continue
|
| 398 |
+
except ValueError:
|
| 399 |
+
# can't compare nested structures containing
|
| 400 |
+
# numpy arrays using `==`
|
| 401 |
+
pass
|
| 402 |
+
|
| 403 |
+
if update_generic(old_obj, new_obj):
|
| 404 |
+
continue
|
| 405 |
+
|
| 406 |
+
try:
|
| 407 |
+
setattr(old, key, getattr(new, key))
|
| 408 |
+
except (AttributeError, TypeError):
|
| 409 |
+
pass # skip non-writable attributes
|
| 410 |
+
|
| 411 |
+
for key in list(new.__dict__.keys()):
|
| 412 |
+
if key not in list(old.__dict__.keys()):
|
| 413 |
+
try:
|
| 414 |
+
setattr(old, key, getattr(new, key))
|
| 415 |
+
except (AttributeError, TypeError):
|
| 416 |
+
pass # skip non-writable attributes
|
| 417 |
+
|
| 418 |
+
# update all instances of class
|
| 419 |
+
update_instances(old, new)
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def update_property(old, new):
|
| 423 |
+
"""Replace get/set/del functions of a property"""
|
| 424 |
+
update_generic(old.fdel, new.fdel)
|
| 425 |
+
update_generic(old.fget, new.fget)
|
| 426 |
+
update_generic(old.fset, new.fset)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def isinstance2(a, b, typ):
|
| 430 |
+
return isinstance(a, typ) and isinstance(b, typ)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
UPDATE_RULES = [
|
| 434 |
+
(lambda a, b: isinstance2(a, b, type), update_class),
|
| 435 |
+
(lambda a, b: isinstance2(a, b, types.FunctionType), update_function),
|
| 436 |
+
(lambda a, b: isinstance2(a, b, property), update_property),
|
| 437 |
+
]
|
| 438 |
+
UPDATE_RULES.extend(
|
| 439 |
+
[
|
| 440 |
+
(
|
| 441 |
+
lambda a, b: isinstance2(a, b, types.MethodType),
|
| 442 |
+
lambda a, b: update_function(a.__func__, b.__func__),
|
| 443 |
+
),
|
| 444 |
+
]
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def update_generic(a, b):
|
| 449 |
+
for type_check, update in UPDATE_RULES:
|
| 450 |
+
if type_check(a, b):
|
| 451 |
+
update(a, b)
|
| 452 |
+
return True
|
| 453 |
+
return False
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
class StrongRef:
|
| 457 |
+
def __init__(self, obj):
|
| 458 |
+
self.obj = obj
|
| 459 |
+
|
| 460 |
+
def __call__(self):
|
| 461 |
+
return self.obj
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
mod_attrs = [
|
| 465 |
+
"__name__",
|
| 466 |
+
"__doc__",
|
| 467 |
+
"__package__",
|
| 468 |
+
"__loader__",
|
| 469 |
+
"__spec__",
|
| 470 |
+
"__file__",
|
| 471 |
+
"__cached__",
|
| 472 |
+
"__builtins__",
|
| 473 |
+
]
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
class ImportFromTracker:
|
| 477 |
+
def __init__(self, imports_froms: dict, symbol_map: dict):
|
| 478 |
+
self.imports_froms = imports_froms
|
| 479 |
+
# symbol_map maps original_name -> list of resolved_names
|
| 480 |
+
self.symbol_map = {}
|
| 481 |
+
if symbol_map:
|
| 482 |
+
for module_name, mappings in symbol_map.items():
|
| 483 |
+
self.symbol_map[module_name] = {}
|
| 484 |
+
for original_name, resolved_names in mappings.items():
|
| 485 |
+
if isinstance(resolved_names, list):
|
| 486 |
+
self.symbol_map[module_name][original_name] = resolved_names[:]
|
| 487 |
+
else:
|
| 488 |
+
self.symbol_map[module_name][original_name] = [resolved_names]
|
| 489 |
+
else:
|
| 490 |
+
self.symbol_map = symbol_map or {}
|
| 491 |
+
|
| 492 |
+
def add_import(
|
| 493 |
+
self, module_name: str, original_name: str, resolved_name: str
|
| 494 |
+
) -> None:
|
| 495 |
+
"""Add an import, handling conflicts with existing imports.
|
| 496 |
+
|
| 497 |
+
This method is called after successful code execution, so we know the import is valid.
|
| 498 |
+
"""
|
| 499 |
+
if module_name not in self.imports_froms:
|
| 500 |
+
self.imports_froms[module_name] = []
|
| 501 |
+
if module_name not in self.symbol_map:
|
| 502 |
+
self.symbol_map[module_name] = {}
|
| 503 |
+
|
| 504 |
+
# Check if there's already a different mapping for the same resolved_name from a different original_name
|
| 505 |
+
# We need to remove any conflicting mappings
|
| 506 |
+
for orig_name, res_names in list(self.symbol_map[module_name].items()):
|
| 507 |
+
if resolved_name in res_names and orig_name != original_name:
|
| 508 |
+
# Remove the conflicting resolved_name from the other original_name's list
|
| 509 |
+
res_names.remove(resolved_name)
|
| 510 |
+
if (
|
| 511 |
+
not res_names
|
| 512 |
+
): # If the list is now empty, remove the original_name entirely
|
| 513 |
+
if orig_name in self.imports_froms[module_name]:
|
| 514 |
+
self.imports_froms[module_name].remove(orig_name)
|
| 515 |
+
del self.symbol_map[module_name][orig_name]
|
| 516 |
+
|
| 517 |
+
# Add the new mapping
|
| 518 |
+
if original_name not in self.imports_froms[module_name]:
|
| 519 |
+
self.imports_froms[module_name].append(original_name)
|
| 520 |
+
|
| 521 |
+
if original_name not in self.symbol_map[module_name]:
|
| 522 |
+
self.symbol_map[module_name][original_name] = []
|
| 523 |
+
|
| 524 |
+
# Add the resolved_name if it's not already in the list
|
| 525 |
+
if resolved_name not in self.symbol_map[module_name][original_name]:
|
| 526 |
+
self.symbol_map[module_name][original_name].append(resolved_name)
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
def append_obj(module, d, name, obj, autoload=False):
|
| 530 |
+
in_module = hasattr(obj, "__module__") and obj.__module__ == module.__name__
|
| 531 |
+
if autoload:
|
| 532 |
+
# check needed for module global built-ins
|
| 533 |
+
if not in_module and name in mod_attrs:
|
| 534 |
+
return False
|
| 535 |
+
else:
|
| 536 |
+
if not in_module:
|
| 537 |
+
return False
|
| 538 |
+
|
| 539 |
+
key = (module.__name__, name)
|
| 540 |
+
try:
|
| 541 |
+
d.setdefault(key, []).append(weakref.ref(obj))
|
| 542 |
+
except TypeError:
|
| 543 |
+
pass
|
| 544 |
+
return True
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
def superreload(
|
| 548 |
+
module, reload=reload, old_objects=None, shell=None, import_from_tracker=None
|
| 549 |
+
):
|
| 550 |
+
"""Enhanced version of the builtin reload function.
|
| 551 |
+
|
| 552 |
+
superreload remembers objects previously in the module, and
|
| 553 |
+
|
| 554 |
+
- upgrades the class dictionary of every old class in the module
|
| 555 |
+
- upgrades the code object of every old function and method
|
| 556 |
+
- clears the module's namespace before reloading
|
| 557 |
+
|
| 558 |
+
"""
|
| 559 |
+
if old_objects is None:
|
| 560 |
+
old_objects = {}
|
| 561 |
+
|
| 562 |
+
# collect old objects in the module
|
| 563 |
+
for name, obj in list(module.__dict__.items()):
|
| 564 |
+
if not append_obj(module, old_objects, name, obj):
|
| 565 |
+
continue
|
| 566 |
+
key = (module.__name__, name)
|
| 567 |
+
try:
|
| 568 |
+
old_objects.setdefault(key, []).append(weakref.ref(obj))
|
| 569 |
+
except TypeError:
|
| 570 |
+
pass
|
| 571 |
+
|
| 572 |
+
# reload module
|
| 573 |
+
try:
|
| 574 |
+
# clear namespace first from old cruft
|
| 575 |
+
old_dict = module.__dict__.copy()
|
| 576 |
+
old_name = module.__name__
|
| 577 |
+
module.__dict__.clear()
|
| 578 |
+
module.__dict__["__name__"] = old_name
|
| 579 |
+
module.__dict__["__loader__"] = old_dict["__loader__"]
|
| 580 |
+
except (TypeError, AttributeError, KeyError):
|
| 581 |
+
pass
|
| 582 |
+
|
| 583 |
+
try:
|
| 584 |
+
module = reload(module)
|
| 585 |
+
except:
|
| 586 |
+
# restore module dictionary on failed reload
|
| 587 |
+
module.__dict__.update(old_dict)
|
| 588 |
+
raise
|
| 589 |
+
|
| 590 |
+
for name, new_obj in list(module.__dict__.items()):
|
| 591 |
+
key = (module.__name__, name)
|
| 592 |
+
if key not in old_objects:
|
| 593 |
+
# here 'shell' acts both as a flag and as an output var
|
| 594 |
+
imports_froms = (
|
| 595 |
+
import_from_tracker.imports_froms if import_from_tracker else None
|
| 596 |
+
)
|
| 597 |
+
symbol_map = import_from_tracker.symbol_map if import_from_tracker else None
|
| 598 |
+
if (
|
| 599 |
+
shell is None
|
| 600 |
+
or name == "Enum"
|
| 601 |
+
or not append_obj(module, old_objects, name, new_obj, True)
|
| 602 |
+
or (
|
| 603 |
+
imports_froms
|
| 604 |
+
and module.__name__ in imports_froms
|
| 605 |
+
and "*" not in imports_froms[module.__name__]
|
| 606 |
+
and name not in imports_froms[module.__name__]
|
| 607 |
+
)
|
| 608 |
+
):
|
| 609 |
+
continue
|
| 610 |
+
|
| 611 |
+
# Handle symbol mapping - now supporting multiple resolved names per original name
|
| 612 |
+
if symbol_map and name in symbol_map.get(module.__name__, {}):
|
| 613 |
+
resolved_names = symbol_map.get(module.__name__, {})[name]
|
| 614 |
+
for resolved_name in resolved_names:
|
| 615 |
+
shell.user_ns[resolved_name] = new_obj
|
| 616 |
+
else:
|
| 617 |
+
shell.user_ns[name] = new_obj
|
| 618 |
+
|
| 619 |
+
new_refs = []
|
| 620 |
+
for old_ref in old_objects[key]:
|
| 621 |
+
old_obj = old_ref()
|
| 622 |
+
if old_obj is None:
|
| 623 |
+
continue
|
| 624 |
+
new_refs.append(old_ref)
|
| 625 |
+
update_generic(old_obj, new_obj)
|
| 626 |
+
|
| 627 |
+
if new_refs:
|
| 628 |
+
old_objects[key] = new_refs
|
| 629 |
+
else:
|
| 630 |
+
del old_objects[key]
|
| 631 |
+
|
| 632 |
+
return module
|
| 633 |
+
|
| 634 |
+
|
| 635 |
+
# ------------------------------------------------------------------------------
|
| 636 |
+
# IPython connectivity
|
| 637 |
+
# ------------------------------------------------------------------------------
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
@magics_class
|
| 641 |
+
class AutoreloadMagics(Magics):
|
| 642 |
+
def __init__(self, *a, **kw):
|
| 643 |
+
super().__init__(*a, **kw)
|
| 644 |
+
self._reloader = ModuleReloader(self.shell)
|
| 645 |
+
self._reloader.check_all = False
|
| 646 |
+
self._reloader.autoload_obj = False
|
| 647 |
+
self.loaded_modules = set(sys.modules)
|
| 648 |
+
|
| 649 |
+
@line_magic
|
| 650 |
+
@magic_arguments.magic_arguments()
|
| 651 |
+
@magic_arguments.argument(
|
| 652 |
+
"mode",
|
| 653 |
+
type=str,
|
| 654 |
+
default="now",
|
| 655 |
+
nargs="?",
|
| 656 |
+
help="""blank or 'now' - Reload all modules (except those excluded by %%aimport)
|
| 657 |
+
automatically now.
|
| 658 |
+
|
| 659 |
+
'0' or 'off' - Disable automatic reloading.
|
| 660 |
+
|
| 661 |
+
'1' or 'explicit' - Reload only modules imported with %%aimport every
|
| 662 |
+
time before executing the Python code typed.
|
| 663 |
+
|
| 664 |
+
'2' or 'all' - Reload all modules (except those excluded by %%aimport)
|
| 665 |
+
every time before executing the Python code typed.
|
| 666 |
+
|
| 667 |
+
'3' or 'complete' - Same as 2/all, but also adds any new
|
| 668 |
+
objects in the module.
|
| 669 |
+
|
| 670 |
+
By default, a newer autoreload algorithm that diffs the module's source code
|
| 671 |
+
with the previous version and only reloads changed parts is applied for modes
|
| 672 |
+
2 and below. To use the original algorithm, add the `-` suffix to the mode,
|
| 673 |
+
e.g. '%autoreload 2-', or pass in --full.
|
| 674 |
+
""",
|
| 675 |
+
)
|
| 676 |
+
@magic_arguments.argument(
|
| 677 |
+
"-p",
|
| 678 |
+
"--print",
|
| 679 |
+
action="store_true",
|
| 680 |
+
default=False,
|
| 681 |
+
help="Show autoreload activity using `print` statements",
|
| 682 |
+
)
|
| 683 |
+
@magic_arguments.argument(
|
| 684 |
+
"-l",
|
| 685 |
+
"--log",
|
| 686 |
+
action="store_true",
|
| 687 |
+
default=False,
|
| 688 |
+
help="Show autoreload activity using the logger",
|
| 689 |
+
)
|
| 690 |
+
@magic_arguments.argument(
|
| 691 |
+
"--hide-errors",
|
| 692 |
+
action="store_true",
|
| 693 |
+
default=False,
|
| 694 |
+
help="Hide autoreload errors",
|
| 695 |
+
)
|
| 696 |
+
@magic_arguments.argument(
|
| 697 |
+
"--full",
|
| 698 |
+
action="store_true",
|
| 699 |
+
default=False,
|
| 700 |
+
help="Don't ever use new diffing algorithm",
|
| 701 |
+
)
|
| 702 |
+
def autoreload(self, line=""):
|
| 703 |
+
r"""%autoreload => Reload modules automatically
|
| 704 |
+
|
| 705 |
+
%autoreload or %autoreload now
|
| 706 |
+
Reload all modules (except those excluded by %aimport) automatically
|
| 707 |
+
now.
|
| 708 |
+
|
| 709 |
+
%autoreload 0 or %autoreload off
|
| 710 |
+
Disable automatic reloading.
|
| 711 |
+
|
| 712 |
+
%autoreload 1 or %autoreload explicit
|
| 713 |
+
Reload only modules imported with %aimport every time before executing
|
| 714 |
+
the Python code typed.
|
| 715 |
+
|
| 716 |
+
%autoreload 2 or %autoreload all
|
| 717 |
+
Reload all modules (except those excluded by %aimport) every time
|
| 718 |
+
before executing the Python code typed.
|
| 719 |
+
|
| 720 |
+
%autoreload 3 or %autoreload complete
|
| 721 |
+
Same as 2/all, but also but also adds any new objects in the module. See
|
| 722 |
+
unit test at IPython/extensions/tests/test_autoreload.py::test_autoload_newly_added_objects
|
| 723 |
+
|
| 724 |
+
The optional arguments --print and --log control display of autoreload activity. The default
|
| 725 |
+
is to act silently; --print (or -p) will print out the names of modules that are being
|
| 726 |
+
reloaded, and --log (or -l) outputs them to the log at INFO level.
|
| 727 |
+
|
| 728 |
+
The optional argument --hide-errors hides any errors that can happen when trying to
|
| 729 |
+
reload code.
|
| 730 |
+
|
| 731 |
+
Reloading Python modules in a reliable way is in general
|
| 732 |
+
difficult, and unexpected things may occur. %autoreload tries to
|
| 733 |
+
work around common pitfalls by replacing function code objects and
|
| 734 |
+
parts of classes previously in the module with new versions. This
|
| 735 |
+
makes the following things to work:
|
| 736 |
+
|
| 737 |
+
- Functions and classes imported via 'from xxx import foo' are upgraded
|
| 738 |
+
to new versions when 'xxx' is reloaded.
|
| 739 |
+
|
| 740 |
+
- Methods and properties of classes are upgraded on reload, so that
|
| 741 |
+
calling 'c.foo()' on an object 'c' created before the reload causes
|
| 742 |
+
the new code for 'foo' to be executed.
|
| 743 |
+
|
| 744 |
+
Some of the known remaining caveats are:
|
| 745 |
+
|
| 746 |
+
- Replacing code objects does not always succeed: changing a @property
|
| 747 |
+
in a class to an ordinary method or a method to a member variable
|
| 748 |
+
can cause problems (but in old objects only).
|
| 749 |
+
|
| 750 |
+
- Functions that are removed (eg. via monkey-patching) from a module
|
| 751 |
+
before it is reloaded are not upgraded.
|
| 752 |
+
|
| 753 |
+
- C extension modules cannot be reloaded, and so cannot be
|
| 754 |
+
autoreloaded.
|
| 755 |
+
|
| 756 |
+
"""
|
| 757 |
+
args = magic_arguments.parse_argstring(self.autoreload, line)
|
| 758 |
+
mode = args.mode.lower()
|
| 759 |
+
|
| 760 |
+
enable_deduperreload = not args.full
|
| 761 |
+
if mode.endswith("-"):
|
| 762 |
+
enable_deduperreload = False
|
| 763 |
+
mode = mode[:-1]
|
| 764 |
+
self._reloader.deduper_reloader.enabled = enable_deduperreload
|
| 765 |
+
|
| 766 |
+
p = print
|
| 767 |
+
|
| 768 |
+
logger = logging.getLogger("autoreload")
|
| 769 |
+
|
| 770 |
+
l = logger.info
|
| 771 |
+
|
| 772 |
+
def pl(msg):
|
| 773 |
+
p(msg)
|
| 774 |
+
l(msg)
|
| 775 |
+
|
| 776 |
+
if args.print is False and args.log is False:
|
| 777 |
+
self._reloader._report = lambda msg: None
|
| 778 |
+
elif args.print is True:
|
| 779 |
+
if args.log is True:
|
| 780 |
+
self._reloader._report = pl
|
| 781 |
+
else:
|
| 782 |
+
self._reloader._report = p
|
| 783 |
+
elif args.log is True:
|
| 784 |
+
self._reloader._report = l
|
| 785 |
+
|
| 786 |
+
self._reloader.hide_errors = args.hide_errors
|
| 787 |
+
|
| 788 |
+
if mode == "" or mode == "now":
|
| 789 |
+
self._reloader.check(True)
|
| 790 |
+
elif mode == "0" or mode == "off":
|
| 791 |
+
self._reloader.enabled = False
|
| 792 |
+
elif mode == "1" or mode == "explicit":
|
| 793 |
+
self._reloader.enabled = True
|
| 794 |
+
self._reloader.check_all = False
|
| 795 |
+
self._reloader.autoload_obj = False
|
| 796 |
+
elif mode == "2" or mode == "all":
|
| 797 |
+
self._reloader.enabled = True
|
| 798 |
+
self._reloader.check_all = True
|
| 799 |
+
self._reloader.autoload_obj = False
|
| 800 |
+
elif mode == "3" or mode == "complete":
|
| 801 |
+
self._reloader.enabled = True
|
| 802 |
+
self._reloader.check_all = True
|
| 803 |
+
self._reloader.autoload_obj = True
|
| 804 |
+
else:
|
| 805 |
+
raise ValueError(f'Unrecognized autoreload mode "{mode}".')
|
| 806 |
+
|
| 807 |
+
@line_magic
|
| 808 |
+
def aimport(self, parameter_s="", stream=None):
|
| 809 |
+
"""%aimport => Import modules for automatic reloading.
|
| 810 |
+
|
| 811 |
+
%aimport
|
| 812 |
+
List modules to automatically import and not to import.
|
| 813 |
+
|
| 814 |
+
%aimport foo
|
| 815 |
+
Import module 'foo' and mark it to be autoreloaded for %autoreload explicit
|
| 816 |
+
|
| 817 |
+
%aimport foo, bar
|
| 818 |
+
Import modules 'foo', 'bar' and mark them to be autoreloaded for %autoreload explicit
|
| 819 |
+
|
| 820 |
+
%aimport -foo, bar
|
| 821 |
+
Mark module 'foo' to not be autoreloaded for %autoreload explicit, all, or complete, and 'bar'
|
| 822 |
+
to be autoreloaded for mode explicit.
|
| 823 |
+
"""
|
| 824 |
+
modname = parameter_s
|
| 825 |
+
if not modname:
|
| 826 |
+
to_reload = sorted(self._reloader.modules.keys())
|
| 827 |
+
to_skip = sorted(self._reloader.skip_modules.keys())
|
| 828 |
+
if stream is None:
|
| 829 |
+
stream = sys.stdout
|
| 830 |
+
if self._reloader.check_all:
|
| 831 |
+
stream.write("Modules to reload:\nall-except-skipped\n")
|
| 832 |
+
else:
|
| 833 |
+
stream.write("Modules to reload:\n%s\n" % " ".join(to_reload))
|
| 834 |
+
stream.write("\nModules to skip:\n%s\n" % " ".join(to_skip))
|
| 835 |
+
else:
|
| 836 |
+
for _module in [_.strip() for _ in modname.split(",")]:
|
| 837 |
+
if _module.startswith("-"):
|
| 838 |
+
_module = _module[1:].strip()
|
| 839 |
+
self._reloader.mark_module_skipped(_module)
|
| 840 |
+
else:
|
| 841 |
+
top_module, top_name = self._reloader.aimport_module(_module)
|
| 842 |
+
|
| 843 |
+
# Inject module to user namespace
|
| 844 |
+
self.shell.push({top_name: top_module})
|
| 845 |
+
|
| 846 |
+
def pre_run_cell(self, info):
|
| 847 |
+
# Store the execution info for later use in post_execute_hook
|
| 848 |
+
self._last_execution_info = info
|
| 849 |
+
|
| 850 |
+
if self._reloader.enabled:
|
| 851 |
+
try:
|
| 852 |
+
self._reloader.check()
|
| 853 |
+
except:
|
| 854 |
+
pass
|
| 855 |
+
|
| 856 |
+
def post_execute_hook(self):
|
| 857 |
+
"""Cache the modification times of any modules imported in this execution and track imports"""
|
| 858 |
+
|
| 859 |
+
# Track imports from the recently executed code if autoreload 3 is enabled
|
| 860 |
+
if self._reloader.enabled and self._reloader.autoload_obj:
|
| 861 |
+
# Use the stored execution info
|
| 862 |
+
if (
|
| 863 |
+
hasattr(self, "_last_execution_info")
|
| 864 |
+
and self._last_execution_info
|
| 865 |
+
and self._last_execution_info.transformed_cell
|
| 866 |
+
):
|
| 867 |
+
self._track_imports_from_code(
|
| 868 |
+
self._last_execution_info.transformed_cell
|
| 869 |
+
)
|
| 870 |
+
|
| 871 |
+
newly_loaded_modules = set(sys.modules) - self.loaded_modules
|
| 872 |
+
for modname in newly_loaded_modules:
|
| 873 |
+
_, pymtime = self._reloader.filename_and_mtime(sys.modules[modname])
|
| 874 |
+
if pymtime is not None:
|
| 875 |
+
self._reloader.modules_mtimes[modname] = pymtime
|
| 876 |
+
|
| 877 |
+
self.loaded_modules.update(newly_loaded_modules)
|
| 878 |
+
|
| 879 |
+
def _track_imports_from_code(self, code: str) -> None:
|
| 880 |
+
"""Track import statements from executed code"""
|
| 881 |
+
try:
|
| 882 |
+
tree = ast.parse(code)
|
| 883 |
+
|
| 884 |
+
for node in ast.walk(tree):
|
| 885 |
+
# Handle "from X import Y" style imports
|
| 886 |
+
if isinstance(node, ast.ImportFrom):
|
| 887 |
+
mod = node.module
|
| 888 |
+
|
| 889 |
+
# Skip relative imports that don't have a module name
|
| 890 |
+
if mod is None:
|
| 891 |
+
continue
|
| 892 |
+
|
| 893 |
+
for name in node.names:
|
| 894 |
+
# name.name is going to be actual name that we want to import from module
|
| 895 |
+
# name.asname is Z in the case of from X import Y as Z
|
| 896 |
+
# we should update Z in the shell in this situation, so track it too.
|
| 897 |
+
original_name = name.name
|
| 898 |
+
resolved_name = name.asname if name.asname else name.name
|
| 899 |
+
|
| 900 |
+
# Since the code executed successfully, we know this import is valid
|
| 901 |
+
self._reloader.import_from_tracker.add_import(
|
| 902 |
+
mod, original_name, resolved_name
|
| 903 |
+
)
|
| 904 |
+
except (SyntaxError, ValueError):
|
| 905 |
+
# If there's a syntax error, skip import tracking
|
| 906 |
+
# (though this shouldn't happen since the code already executed successfully)
|
| 907 |
+
pass
|
| 908 |
+
|
| 909 |
+
|
| 910 |
+
def load_ipython_extension(ip):
|
| 911 |
+
"""Load the extension in IPython."""
|
| 912 |
+
auto_reload = AutoreloadMagics(ip)
|
| 913 |
+
ip.register_magics(auto_reload)
|
| 914 |
+
ip.events.register("pre_run_cell", auto_reload.pre_run_cell)
|
| 915 |
+
ip.events.register("post_execute", auto_reload.post_execute_hook)
|
lib/python3.12/site-packages/IPython/extensions/deduperreload/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (211 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload.cpython-312.pyc
ADDED
|
Binary file (33.2 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/extensions/deduperreload/__pycache__/deduperreload_patching.cpython-312.pyc
ADDED
|
Binary file (6.74 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload.py
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import ast
|
| 3 |
+
import builtins
|
| 4 |
+
import contextlib
|
| 5 |
+
import itertools
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import pickle
|
| 9 |
+
import platform
|
| 10 |
+
import sys
|
| 11 |
+
import textwrap
|
| 12 |
+
from types import ModuleType
|
| 13 |
+
from typing import TYPE_CHECKING, Any, NamedTuple, cast
|
| 14 |
+
from collections.abc import Generator, Iterable
|
| 15 |
+
|
| 16 |
+
from IPython.extensions.deduperreload.deduperreload_patching import (
|
| 17 |
+
DeduperReloaderPatchingMixin,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
if TYPE_CHECKING:
|
| 21 |
+
TDefinitionAst = (
|
| 22 |
+
ast.FunctionDef
|
| 23 |
+
| ast.AsyncFunctionDef
|
| 24 |
+
| ast.Import
|
| 25 |
+
| ast.ImportFrom
|
| 26 |
+
| ast.Assign
|
| 27 |
+
| ast.AnnAssign
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_module_file_name(module: ModuleType | str) -> str | None:
|
| 32 |
+
"""Returns the module's file path, or the empty string if it's inaccessible"""
|
| 33 |
+
if (mod := sys.modules.get(module) if isinstance(module, str) else module) is None:
|
| 34 |
+
return ""
|
| 35 |
+
return getattr(mod, "__file__", "") or ""
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def compare_ast(node1: ast.AST | list[ast.AST], node2: ast.AST | list[ast.AST]) -> bool:
|
| 39 |
+
"""Checks if node1 and node2 have identical AST structure/values, apart from some attributes"""
|
| 40 |
+
if type(node1) is not type(node2):
|
| 41 |
+
return False
|
| 42 |
+
|
| 43 |
+
if isinstance(node1, ast.AST):
|
| 44 |
+
for k, v in node1.__dict__.items():
|
| 45 |
+
if k in (
|
| 46 |
+
"lineno",
|
| 47 |
+
"end_lineno",
|
| 48 |
+
"col_offset",
|
| 49 |
+
"end_col_offset",
|
| 50 |
+
"ctx",
|
| 51 |
+
"parent",
|
| 52 |
+
):
|
| 53 |
+
continue
|
| 54 |
+
if not hasattr(node2, k) or not compare_ast(v, getattr(node2, k)):
|
| 55 |
+
return False
|
| 56 |
+
return True
|
| 57 |
+
|
| 58 |
+
elif isinstance(node1, list) and isinstance( # type:ignore [redundant-expr]
|
| 59 |
+
node2, list
|
| 60 |
+
):
|
| 61 |
+
return len(node1) == len(node2) and all(
|
| 62 |
+
compare_ast(n1, n2) for n1, n2 in zip(node1, node2)
|
| 63 |
+
)
|
| 64 |
+
else:
|
| 65 |
+
return node1 == node2
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class DependencyNode(NamedTuple):
|
| 69 |
+
"""
|
| 70 |
+
Each node represents a function.
|
| 71 |
+
qualified_name: string which represents the namespace/name of the function
|
| 72 |
+
abstract_syntax_tree: subtree of the overall module which corresponds to this function
|
| 73 |
+
|
| 74 |
+
qualified_name is of the structure: (namespace1, namespace2, ..., name)
|
| 75 |
+
|
| 76 |
+
For example, foo() in the following would be represented as (A, B, foo):
|
| 77 |
+
|
| 78 |
+
class A:
|
| 79 |
+
class B:
|
| 80 |
+
def foo():
|
| 81 |
+
pass
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
qualified_name: tuple[str, ...]
|
| 85 |
+
abstract_syntax_tree: ast.AST
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class GatherResult(NamedTuple):
|
| 89 |
+
import_defs: list[tuple[tuple[str, ...], ast.Import | ast.ImportFrom]] = []
|
| 90 |
+
assign_defs: list[tuple[tuple[str, ...], ast.Assign | ast.AnnAssign]] = []
|
| 91 |
+
function_defs: list[
|
| 92 |
+
tuple[tuple[str, ...], ast.FunctionDef | ast.AsyncFunctionDef]
|
| 93 |
+
] = []
|
| 94 |
+
classes: dict[str, ast.ClassDef] = {}
|
| 95 |
+
unfixable: list[ast.AST] = []
|
| 96 |
+
|
| 97 |
+
@classmethod
|
| 98 |
+
def create(cls) -> GatherResult:
|
| 99 |
+
return cls([], [], [], {}, [])
|
| 100 |
+
|
| 101 |
+
def all_defs(self) -> Iterable[tuple[tuple[str, ...], TDefinitionAst]]:
|
| 102 |
+
return itertools.chain(self.import_defs, self.assign_defs, self.function_defs)
|
| 103 |
+
|
| 104 |
+
def inplace_merge(self, other: GatherResult) -> None:
|
| 105 |
+
self.import_defs.extend(other.import_defs)
|
| 106 |
+
self.assign_defs.extend(other.assign_defs)
|
| 107 |
+
self.function_defs.extend(other.function_defs)
|
| 108 |
+
self.classes.update(other.classes)
|
| 109 |
+
self.unfixable.extend(other.unfixable)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class ConstexprDetector(ast.NodeVisitor):
|
| 113 |
+
def __init__(self) -> None:
|
| 114 |
+
self.is_constexpr = True
|
| 115 |
+
self._allow_builtins_exceptions = True
|
| 116 |
+
|
| 117 |
+
@contextlib.contextmanager
|
| 118 |
+
def disallow_builtins_exceptions(self) -> Generator[None, None, None]:
|
| 119 |
+
prev_allow = self._allow_builtins_exceptions
|
| 120 |
+
self._allow_builtins_exceptions = False
|
| 121 |
+
try:
|
| 122 |
+
yield
|
| 123 |
+
finally:
|
| 124 |
+
self._allow_builtins_exceptions = prev_allow
|
| 125 |
+
|
| 126 |
+
def visit_Attribute(self, node: ast.Attribute) -> None:
|
| 127 |
+
with self.disallow_builtins_exceptions():
|
| 128 |
+
self.visit(node.value)
|
| 129 |
+
|
| 130 |
+
def visit_Name(self, node: ast.Name) -> None:
|
| 131 |
+
if self._allow_builtins_exceptions and hasattr(builtins, node.id):
|
| 132 |
+
return
|
| 133 |
+
self.is_constexpr = False
|
| 134 |
+
|
| 135 |
+
def visit(self, node: ast.AST) -> None:
|
| 136 |
+
if not self.is_constexpr:
|
| 137 |
+
# can short-circuit if we've already detected that it's not a constexpr
|
| 138 |
+
return
|
| 139 |
+
super().visit(node)
|
| 140 |
+
|
| 141 |
+
def __call__(self, node: ast.AST) -> bool:
|
| 142 |
+
self.is_constexpr = True
|
| 143 |
+
self.visit(node)
|
| 144 |
+
return self.is_constexpr
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
class AutoreloadTree:
|
| 148 |
+
"""
|
| 149 |
+
Recursive data structure to keep track of reloadable functions/methods. Each object corresponds to a specific scope level.
|
| 150 |
+
children: classes inside given scope, maps class name to autoreload tree for that class's scope
|
| 151 |
+
funcs_to_autoreload: list of function names that can be autoreloaded in given scope.
|
| 152 |
+
new_nested_classes: Classes getting added in new autoreload cycle
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
def __init__(self) -> None:
|
| 156 |
+
self.children: dict[str, AutoreloadTree] = {}
|
| 157 |
+
self.defs_to_reload: list[tuple[tuple[str, ...], ast.AST]] = []
|
| 158 |
+
self.defs_to_delete: set[str] = set()
|
| 159 |
+
self.new_nested_classes: dict[str, ast.AST] = {}
|
| 160 |
+
|
| 161 |
+
def traverse_prefixes(self, prefixes: list[str]) -> AutoreloadTree:
|
| 162 |
+
"""
|
| 163 |
+
Return ref to the AutoreloadTree at the namespace specified by prefixes
|
| 164 |
+
"""
|
| 165 |
+
cur = self
|
| 166 |
+
for prefix in prefixes:
|
| 167 |
+
if prefix not in cur.children:
|
| 168 |
+
cur.children[prefix] = AutoreloadTree()
|
| 169 |
+
cur = cur.children[prefix]
|
| 170 |
+
return cur
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class DeduperReloader(DeduperReloaderPatchingMixin):
|
| 174 |
+
"""
|
| 175 |
+
This version of autoreload detects when we can leverage targeted recompilation of a subset of a module and patching
|
| 176 |
+
existing function/method objects to reflect these changes.
|
| 177 |
+
|
| 178 |
+
Detects what functions/methods can be reloaded by recursively comparing the old/new AST of module-level classes,
|
| 179 |
+
module-level classes' methods, recursing through nested classes' methods. If other changes are made, original
|
| 180 |
+
autoreload algorithm is called directly.
|
| 181 |
+
"""
|
| 182 |
+
|
| 183 |
+
def __init__(self) -> None:
|
| 184 |
+
self._to_autoreload: AutoreloadTree = AutoreloadTree()
|
| 185 |
+
self.source_by_modname: dict[str, str] = {}
|
| 186 |
+
self.dependency_graph: dict[tuple[str, ...], list[DependencyNode]] = {}
|
| 187 |
+
self._enabled = True
|
| 188 |
+
|
| 189 |
+
@property
|
| 190 |
+
def enabled(self) -> bool:
|
| 191 |
+
return self._enabled and platform.python_implementation() == "CPython"
|
| 192 |
+
|
| 193 |
+
@enabled.setter
|
| 194 |
+
def enabled(self, value: bool) -> None:
|
| 195 |
+
self._enabled = value
|
| 196 |
+
|
| 197 |
+
def update_sources(self) -> None:
|
| 198 |
+
"""
|
| 199 |
+
Update dictionary source_by_modname with current modules' source codes.
|
| 200 |
+
"""
|
| 201 |
+
if not self.enabled:
|
| 202 |
+
return
|
| 203 |
+
for new_modname in sys.modules.keys() - self.source_by_modname.keys():
|
| 204 |
+
new_module = sys.modules[new_modname]
|
| 205 |
+
if (
|
| 206 |
+
(fname := get_module_file_name(new_module)) is None
|
| 207 |
+
or "site-packages" in fname
|
| 208 |
+
or "dist-packages" in fname
|
| 209 |
+
or not os.access(fname, os.R_OK)
|
| 210 |
+
):
|
| 211 |
+
self.source_by_modname[new_modname] = ""
|
| 212 |
+
continue
|
| 213 |
+
# Skip binary files (e.g., .so, .pyd, .pyo)
|
| 214 |
+
if not fname.endswith(".py"):
|
| 215 |
+
self.source_by_modname[new_modname] = ""
|
| 216 |
+
continue
|
| 217 |
+
try:
|
| 218 |
+
with open(fname, "r") as f:
|
| 219 |
+
self.source_by_modname[new_modname] = f.read()
|
| 220 |
+
except Exception as e:
|
| 221 |
+
logger = logging.getLogger("autoreload")
|
| 222 |
+
logger.exception(
|
| 223 |
+
f"Failed to read module file '{fname}' for module '{new_modname}': {type(e).__name__}"
|
| 224 |
+
)
|
| 225 |
+
self.source_by_modname[new_modname] = ""
|
| 226 |
+
|
| 227 |
+
constexpr_detector = ConstexprDetector()
|
| 228 |
+
|
| 229 |
+
@staticmethod
|
| 230 |
+
def is_enum_subclass(node: ast.Module | ast.ClassDef) -> bool:
|
| 231 |
+
if isinstance(node, ast.Module):
|
| 232 |
+
return False
|
| 233 |
+
for base in node.bases:
|
| 234 |
+
if isinstance(base, ast.Name) and base.id == "Enum":
|
| 235 |
+
return True
|
| 236 |
+
elif (
|
| 237 |
+
isinstance(base, ast.Attribute)
|
| 238 |
+
and base.attr == "Enum"
|
| 239 |
+
and isinstance(base.value, ast.Name)
|
| 240 |
+
and base.value.id == "enum"
|
| 241 |
+
):
|
| 242 |
+
return True
|
| 243 |
+
return False
|
| 244 |
+
|
| 245 |
+
@classmethod
|
| 246 |
+
def is_constexpr_assign(
|
| 247 |
+
cls, node: ast.AST, parent_node: ast.Module | ast.ClassDef
|
| 248 |
+
) -> bool:
|
| 249 |
+
if not isinstance(node, (ast.Assign, ast.AnnAssign)) or node.value is None:
|
| 250 |
+
return False
|
| 251 |
+
if cls.is_enum_subclass(parent_node):
|
| 252 |
+
return False
|
| 253 |
+
for target in node.targets if isinstance(node, ast.Assign) else [node.target]:
|
| 254 |
+
if not isinstance(target, ast.Name):
|
| 255 |
+
return False
|
| 256 |
+
return cls.constexpr_detector(node.value)
|
| 257 |
+
|
| 258 |
+
@classmethod
|
| 259 |
+
def _gather_children(
|
| 260 |
+
cls, body: list[ast.stmt], parent_node: ast.Module | ast.ClassDef
|
| 261 |
+
) -> GatherResult:
|
| 262 |
+
"""
|
| 263 |
+
Given list of ast elements, return:
|
| 264 |
+
1. dict mapping function names to their ASTs.
|
| 265 |
+
2. dict mapping class names to their ASTs.
|
| 266 |
+
3. list of any other ASTs.
|
| 267 |
+
"""
|
| 268 |
+
result = GatherResult.create()
|
| 269 |
+
for ast_node in body:
|
| 270 |
+
ast_elt: ast.expr | ast.stmt = ast_node
|
| 271 |
+
while isinstance(ast_elt, ast.Expr):
|
| 272 |
+
ast_elt = ast_elt.value
|
| 273 |
+
if isinstance(ast_elt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 274 |
+
result.function_defs.append(((ast_elt.name,), ast_elt))
|
| 275 |
+
elif isinstance(ast_elt, (ast.Import, ast.ImportFrom)):
|
| 276 |
+
result.import_defs.append(
|
| 277 |
+
(tuple(name.asname or name.name for name in ast_elt.names), ast_elt)
|
| 278 |
+
)
|
| 279 |
+
elif isinstance(ast_elt, ast.ClassDef):
|
| 280 |
+
result.classes[ast_elt.name] = ast_elt
|
| 281 |
+
elif isinstance(ast_elt, ast.If):
|
| 282 |
+
result.unfixable.append(ast_elt.test)
|
| 283 |
+
result.inplace_merge(cls._gather_children(ast_elt.body, parent_node))
|
| 284 |
+
result.inplace_merge(cls._gather_children(ast_elt.orelse, parent_node))
|
| 285 |
+
elif isinstance(ast_elt, (ast.AsyncWith, ast.With)):
|
| 286 |
+
result.unfixable.extend(ast_elt.items)
|
| 287 |
+
result.inplace_merge(cls._gather_children(ast_elt.body, parent_node))
|
| 288 |
+
elif isinstance(ast_elt, ast.Try):
|
| 289 |
+
result.inplace_merge(cls._gather_children(ast_elt.body, parent_node))
|
| 290 |
+
result.inplace_merge(cls._gather_children(ast_elt.orelse, parent_node))
|
| 291 |
+
result.inplace_merge(
|
| 292 |
+
cls._gather_children(ast_elt.finalbody, parent_node)
|
| 293 |
+
)
|
| 294 |
+
for handler in ast_elt.handlers:
|
| 295 |
+
if handler.type is not None:
|
| 296 |
+
result.unfixable.append(handler.type)
|
| 297 |
+
result.inplace_merge(
|
| 298 |
+
cls._gather_children(handler.body, parent_node)
|
| 299 |
+
)
|
| 300 |
+
elif not isinstance(ast_elt, (ast.Constant, ast.Pass)):
|
| 301 |
+
if cls.is_constexpr_assign(ast_elt, parent_node):
|
| 302 |
+
assert isinstance(ast_elt, (ast.Assign, ast.AnnAssign))
|
| 303 |
+
targets = (
|
| 304 |
+
ast_elt.targets
|
| 305 |
+
if isinstance(ast_elt, ast.Assign)
|
| 306 |
+
else [ast_elt.target]
|
| 307 |
+
)
|
| 308 |
+
result.assign_defs.append(
|
| 309 |
+
(
|
| 310 |
+
tuple(cast(ast.Name, target).id for target in targets),
|
| 311 |
+
ast_elt,
|
| 312 |
+
)
|
| 313 |
+
)
|
| 314 |
+
else:
|
| 315 |
+
result.unfixable.append(ast_elt)
|
| 316 |
+
return result
|
| 317 |
+
|
| 318 |
+
def detect_autoreload(
|
| 319 |
+
self,
|
| 320 |
+
old_node: ast.Module | ast.ClassDef,
|
| 321 |
+
new_node: ast.Module | ast.ClassDef,
|
| 322 |
+
prefixes: list[str] | None = None,
|
| 323 |
+
) -> bool:
|
| 324 |
+
"""
|
| 325 |
+
Returns
|
| 326 |
+
-------
|
| 327 |
+
`True` if we can run our targeted autoreload algorithm safely.
|
| 328 |
+
`False` if we should instead use IPython's original autoreload implementation.
|
| 329 |
+
"""
|
| 330 |
+
if not self.enabled:
|
| 331 |
+
return False
|
| 332 |
+
prefixes = prefixes or []
|
| 333 |
+
|
| 334 |
+
old_result = self._gather_children(old_node.body, old_node)
|
| 335 |
+
new_result = self._gather_children(new_node.body, new_node)
|
| 336 |
+
old_defs_by_name: dict[str, ast.AST] = {
|
| 337 |
+
name: ast_def for names, ast_def in old_result.all_defs() for name in names
|
| 338 |
+
}
|
| 339 |
+
new_defs_by_name: dict[str, ast.AST] = {
|
| 340 |
+
name: ast_def for names, ast_def in new_result.all_defs() for name in names
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
if not compare_ast(old_result.unfixable, new_result.unfixable):
|
| 344 |
+
return False
|
| 345 |
+
|
| 346 |
+
cur = self._to_autoreload.traverse_prefixes(prefixes)
|
| 347 |
+
for names, new_ast_def in new_result.all_defs():
|
| 348 |
+
names_to_reload = []
|
| 349 |
+
for name in names:
|
| 350 |
+
if new_defs_by_name[name] is not new_ast_def:
|
| 351 |
+
continue
|
| 352 |
+
if name not in old_defs_by_name or not compare_ast(
|
| 353 |
+
new_ast_def, old_defs_by_name[name]
|
| 354 |
+
):
|
| 355 |
+
names_to_reload.append(name)
|
| 356 |
+
if names_to_reload:
|
| 357 |
+
cur.defs_to_reload.append((tuple(names), new_ast_def))
|
| 358 |
+
cur.defs_to_delete |= set(old_defs_by_name.keys()) - set(
|
| 359 |
+
new_defs_by_name.keys()
|
| 360 |
+
)
|
| 361 |
+
for name, new_ast_def_class in new_result.classes.items():
|
| 362 |
+
if name not in old_result.classes:
|
| 363 |
+
cur.new_nested_classes[name] = new_ast_def_class
|
| 364 |
+
elif not compare_ast(
|
| 365 |
+
new_ast_def_class, old_result.classes[name]
|
| 366 |
+
) and not self.detect_autoreload(
|
| 367 |
+
old_result.classes[name], new_ast_def_class, prefixes + [name]
|
| 368 |
+
):
|
| 369 |
+
return False
|
| 370 |
+
return True
|
| 371 |
+
|
| 372 |
+
def _check_dependents(self) -> bool:
|
| 373 |
+
"""
|
| 374 |
+
If a decorator function is modified, we should similarly reload the functions which are decorated by this
|
| 375 |
+
decorator. Iterate through the Dependency Graph to find such cases in the given AutoreloadTree.
|
| 376 |
+
"""
|
| 377 |
+
for node in self._check_dependents_inner():
|
| 378 |
+
self._add_node_to_autoreload_tree(node)
|
| 379 |
+
return True
|
| 380 |
+
|
| 381 |
+
def _add_node_to_autoreload_tree(self, node: DependencyNode) -> None:
|
| 382 |
+
"""
|
| 383 |
+
Given a node of the dependency graph, add decorator dependencies to the autoreload tree.
|
| 384 |
+
"""
|
| 385 |
+
if len(node.qualified_name) == 0:
|
| 386 |
+
return
|
| 387 |
+
cur = self._to_autoreload.traverse_prefixes(list(node.qualified_name[:-1]))
|
| 388 |
+
if node.abstract_syntax_tree is not None:
|
| 389 |
+
cur.defs_to_reload.append(
|
| 390 |
+
((node.qualified_name[-1],), node.abstract_syntax_tree)
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
def _check_dependents_inner(
|
| 394 |
+
self, prefixes: list[str] | None = None
|
| 395 |
+
) -> list[DependencyNode]:
|
| 396 |
+
prefixes = prefixes or []
|
| 397 |
+
cur = self._to_autoreload.traverse_prefixes(prefixes)
|
| 398 |
+
ans = []
|
| 399 |
+
for (func_name, *_), _ in cur.defs_to_reload:
|
| 400 |
+
node = tuple(prefixes + [func_name])
|
| 401 |
+
ans.extend(self._gen_dependents(node))
|
| 402 |
+
for class_name in cur.new_nested_classes:
|
| 403 |
+
ans.extend(self._check_dependents_inner(prefixes + [class_name]))
|
| 404 |
+
return ans
|
| 405 |
+
|
| 406 |
+
def _gen_dependents(self, qualname: tuple[str, ...]) -> list[DependencyNode]:
|
| 407 |
+
ans = []
|
| 408 |
+
if qualname not in self.dependency_graph:
|
| 409 |
+
return []
|
| 410 |
+
for elt in self.dependency_graph[qualname]:
|
| 411 |
+
ans.extend(self._gen_dependents(elt.qualified_name))
|
| 412 |
+
ans.append(elt)
|
| 413 |
+
return ans
|
| 414 |
+
|
| 415 |
+
def _patch_namespace_inner(
|
| 416 |
+
self, ns: ModuleType | type, prefixes: list[str] | None = None
|
| 417 |
+
) -> bool:
|
| 418 |
+
"""
|
| 419 |
+
This function patches module functions and methods. Specifically, only objects with their name in
|
| 420 |
+
self.to_autoreload will be considered for patching. If an object has been marked to be autoreloaded,
|
| 421 |
+
new_source_code gets executed in the old version's global environment. Then, replace the old function's
|
| 422 |
+
attributes with the new function's attributes.
|
| 423 |
+
"""
|
| 424 |
+
prefixes = prefixes or []
|
| 425 |
+
cur = self._to_autoreload.traverse_prefixes(prefixes)
|
| 426 |
+
namespace_to_check = ns
|
| 427 |
+
for prefix in prefixes:
|
| 428 |
+
namespace_to_check = namespace_to_check.__dict__[prefix]
|
| 429 |
+
seen_names: set[str] = set()
|
| 430 |
+
for names, new_ast_def in cur.defs_to_reload:
|
| 431 |
+
if len(names) == 1 and names[0] in seen_names:
|
| 432 |
+
continue
|
| 433 |
+
seen_names.update(names)
|
| 434 |
+
local_env: dict[str, Any] = {}
|
| 435 |
+
if (
|
| 436 |
+
isinstance(new_ast_def, (ast.FunctionDef, ast.AsyncFunctionDef))
|
| 437 |
+
and (name := names[0]) in namespace_to_check.__dict__
|
| 438 |
+
):
|
| 439 |
+
assert len(names) == 1
|
| 440 |
+
to_patch_to = namespace_to_check.__dict__[name]
|
| 441 |
+
if isinstance(to_patch_to, (staticmethod, classmethod)):
|
| 442 |
+
to_patch_to = to_patch_to.__func__
|
| 443 |
+
# exec new source code using old function's (obj) globals environment.
|
| 444 |
+
func_code = textwrap.dedent(ast.unparse(new_ast_def))
|
| 445 |
+
if is_method := (len(prefixes) > 0):
|
| 446 |
+
func_code = "class __autoreload_class__:\n" + textwrap.indent(
|
| 447 |
+
func_code, " "
|
| 448 |
+
)
|
| 449 |
+
global_env = ns.__dict__
|
| 450 |
+
if not isinstance(global_env, dict):
|
| 451 |
+
global_env = dict(global_env)
|
| 452 |
+
# Compile with correct filename to preserve in traceback
|
| 453 |
+
filename = (
|
| 454 |
+
getattr(to_patch_to, "__code__", None)
|
| 455 |
+
and to_patch_to.__code__.co_filename
|
| 456 |
+
or "<string>"
|
| 457 |
+
)
|
| 458 |
+
func_asts = [ast.parse(func_code)]
|
| 459 |
+
if len(cast(ast.FunctionDef, func_asts[0].body[0]).decorator_list) > 0:
|
| 460 |
+
without_decorator_list = pickle.loads(pickle.dumps(func_asts[0]))
|
| 461 |
+
cast(
|
| 462 |
+
ast.FunctionDef, without_decorator_list.body[0]
|
| 463 |
+
).decorator_list = []
|
| 464 |
+
func_asts.insert(0, without_decorator_list)
|
| 465 |
+
for func_ast in func_asts:
|
| 466 |
+
compiled_code = compile(
|
| 467 |
+
func_ast, filename, mode="exec", dont_inherit=True
|
| 468 |
+
)
|
| 469 |
+
exec(compiled_code, global_env, local_env) # type: ignore[arg-type]
|
| 470 |
+
# local_env contains the function exec'd from new version of function
|
| 471 |
+
if is_method:
|
| 472 |
+
to_patch_from = getattr(local_env["__autoreload_class__"], name)
|
| 473 |
+
else:
|
| 474 |
+
to_patch_from = local_env[name]
|
| 475 |
+
if isinstance(to_patch_from, (staticmethod, classmethod)):
|
| 476 |
+
to_patch_from = to_patch_from.__func__
|
| 477 |
+
if isinstance(to_patch_to, property) and isinstance(
|
| 478 |
+
to_patch_from, property
|
| 479 |
+
):
|
| 480 |
+
for attr in ("fget", "fset", "fdel"):
|
| 481 |
+
if (
|
| 482 |
+
getattr(to_patch_to, attr) is None
|
| 483 |
+
or getattr(to_patch_from, attr) is None
|
| 484 |
+
):
|
| 485 |
+
self.try_patch_attr(to_patch_to, to_patch_from, attr)
|
| 486 |
+
else:
|
| 487 |
+
self.patch_function(
|
| 488 |
+
getattr(to_patch_to, attr),
|
| 489 |
+
getattr(to_patch_from, attr),
|
| 490 |
+
is_method,
|
| 491 |
+
)
|
| 492 |
+
elif not isinstance(to_patch_to, property) and not isinstance(
|
| 493 |
+
to_patch_from, property
|
| 494 |
+
):
|
| 495 |
+
self.patch_function(to_patch_to, to_patch_from, is_method)
|
| 496 |
+
else:
|
| 497 |
+
raise ValueError(
|
| 498 |
+
"adding or removing property decorations not supported"
|
| 499 |
+
)
|
| 500 |
+
else:
|
| 501 |
+
exec(
|
| 502 |
+
ast.unparse(new_ast_def),
|
| 503 |
+
ns.__dict__ | namespace_to_check.__dict__,
|
| 504 |
+
local_env,
|
| 505 |
+
)
|
| 506 |
+
for name in names:
|
| 507 |
+
setattr(namespace_to_check, name, local_env[name])
|
| 508 |
+
cur.defs_to_reload.clear()
|
| 509 |
+
for name in cur.defs_to_delete:
|
| 510 |
+
try:
|
| 511 |
+
delattr(namespace_to_check, name)
|
| 512 |
+
except (AttributeError, TypeError, ValueError):
|
| 513 |
+
# give up on deleting the attribute, let the stale one dangle
|
| 514 |
+
pass
|
| 515 |
+
cur.defs_to_delete.clear()
|
| 516 |
+
for class_name, class_ast_node in cur.new_nested_classes.items():
|
| 517 |
+
local_env_class: dict[str, Any] = {}
|
| 518 |
+
exec(
|
| 519 |
+
ast.unparse(class_ast_node),
|
| 520 |
+
ns.__dict__ | namespace_to_check.__dict__,
|
| 521 |
+
local_env_class,
|
| 522 |
+
)
|
| 523 |
+
setattr(namespace_to_check, class_name, local_env_class[class_name])
|
| 524 |
+
cur.new_nested_classes.clear()
|
| 525 |
+
for class_name in cur.children.keys():
|
| 526 |
+
if not self._patch_namespace(ns, prefixes + [class_name]):
|
| 527 |
+
return False
|
| 528 |
+
cur.children.clear()
|
| 529 |
+
return True
|
| 530 |
+
|
| 531 |
+
def _patch_namespace(
|
| 532 |
+
self, ns: ModuleType | type, prefixes: list[str] | None = None
|
| 533 |
+
) -> bool:
|
| 534 |
+
"""
|
| 535 |
+
Wrapper for patching all elements in a namespace as specified by the to_autoreload member variable.
|
| 536 |
+
Returns `true` if patching was successful, and `false` if unsuccessful.
|
| 537 |
+
"""
|
| 538 |
+
try:
|
| 539 |
+
return self._patch_namespace_inner(ns, prefixes=prefixes)
|
| 540 |
+
except Exception:
|
| 541 |
+
return False
|
| 542 |
+
|
| 543 |
+
def maybe_reload_module(self, module: ModuleType) -> bool:
|
| 544 |
+
"""
|
| 545 |
+
Uses Deduperreload to try to update a module.
|
| 546 |
+
Returns `true` on success and `false` on failure.
|
| 547 |
+
"""
|
| 548 |
+
if not self.enabled:
|
| 549 |
+
return False
|
| 550 |
+
if not (modname := getattr(module, "__name__", None)):
|
| 551 |
+
return False
|
| 552 |
+
if (fname := get_module_file_name(module)) is None:
|
| 553 |
+
return False
|
| 554 |
+
try:
|
| 555 |
+
with open(fname, "r") as f:
|
| 556 |
+
new_source_code = f.read()
|
| 557 |
+
except Exception as e:
|
| 558 |
+
logger = logging.getLogger("autoreload")
|
| 559 |
+
logger.exception(
|
| 560 |
+
f"Failed to read module file '{fname}' for module '{modname}': {type(e).__name__}"
|
| 561 |
+
)
|
| 562 |
+
return False
|
| 563 |
+
patched_flag = False
|
| 564 |
+
if old_source_code := self.source_by_modname.get(modname):
|
| 565 |
+
# get old/new module ast
|
| 566 |
+
try:
|
| 567 |
+
old_module_ast = ast.parse(old_source_code)
|
| 568 |
+
new_module_ast = ast.parse(new_source_code)
|
| 569 |
+
except Exception:
|
| 570 |
+
return False
|
| 571 |
+
# detect if we are able to use our autoreload algorithm
|
| 572 |
+
ctx = contextlib.suppress()
|
| 573 |
+
with ctx:
|
| 574 |
+
self._build_dependency_graph(new_module_ast)
|
| 575 |
+
if (
|
| 576 |
+
self.detect_autoreload(old_module_ast, new_module_ast)
|
| 577 |
+
and self._check_dependents()
|
| 578 |
+
and self._patch_namespace(module)
|
| 579 |
+
):
|
| 580 |
+
patched_flag = True
|
| 581 |
+
|
| 582 |
+
self.source_by_modname[modname] = new_source_code
|
| 583 |
+
self._to_autoreload = AutoreloadTree()
|
| 584 |
+
return patched_flag
|
| 585 |
+
|
| 586 |
+
def _separate_name(
|
| 587 |
+
self,
|
| 588 |
+
decorator: ast.Attribute | ast.Name | ast.Call | ast.expr,
|
| 589 |
+
accept_calls: bool,
|
| 590 |
+
) -> list[str] | None:
|
| 591 |
+
"""
|
| 592 |
+
Generates a qualified name for a given decorator by finding its relative namespace.
|
| 593 |
+
"""
|
| 594 |
+
if isinstance(decorator, ast.Name):
|
| 595 |
+
return [decorator.id]
|
| 596 |
+
elif isinstance(decorator, ast.Call):
|
| 597 |
+
if accept_calls:
|
| 598 |
+
return self._separate_name(decorator.func, False)
|
| 599 |
+
else:
|
| 600 |
+
return None
|
| 601 |
+
if not isinstance(decorator, ast.Attribute):
|
| 602 |
+
return None
|
| 603 |
+
if pref := self._separate_name(decorator.value, False):
|
| 604 |
+
return pref + [decorator.attr]
|
| 605 |
+
else:
|
| 606 |
+
return None
|
| 607 |
+
|
| 608 |
+
def _gather_dependents(
|
| 609 |
+
self, body: list[ast.stmt], body_prefixes: list[str] | None = None
|
| 610 |
+
) -> bool:
|
| 611 |
+
body_prefixes = body_prefixes or []
|
| 612 |
+
for ast_node in body:
|
| 613 |
+
ast_elt: ast.expr | ast.stmt = ast_node
|
| 614 |
+
if isinstance(ast_elt, ast.ClassDef):
|
| 615 |
+
self._gather_dependents(ast_elt.body, body_prefixes + [ast_elt.name])
|
| 616 |
+
continue
|
| 617 |
+
if not isinstance(ast_elt, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
| 618 |
+
continue
|
| 619 |
+
qualified_name = tuple(body_prefixes + [ast_elt.name])
|
| 620 |
+
cur_dependency_node = DependencyNode(qualified_name, ast_elt)
|
| 621 |
+
for decorator in ast_elt.decorator_list:
|
| 622 |
+
decorator_path = self._separate_name(decorator, True)
|
| 623 |
+
if not decorator_path:
|
| 624 |
+
continue
|
| 625 |
+
decorator_path_tuple = tuple(decorator_path)
|
| 626 |
+
self.dependency_graph.setdefault(decorator_path_tuple, []).append(
|
| 627 |
+
cur_dependency_node
|
| 628 |
+
)
|
| 629 |
+
return True
|
| 630 |
+
|
| 631 |
+
def _build_dependency_graph(self, new_ast: ast.Module | ast.ClassDef) -> bool:
|
| 632 |
+
"""
|
| 633 |
+
Wrapper function for generating dependency graph given some AST.
|
| 634 |
+
Returns `true` on success. Returns `false` on failure.
|
| 635 |
+
Currently, only returns `true` as we do not block on failure to build this graph.
|
| 636 |
+
"""
|
| 637 |
+
return self._gather_dependents(new_ast.body)
|
lib/python3.12/site-packages/IPython/extensions/deduperreload/deduperreload_patching.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
import ctypes
|
| 3 |
+
import sys
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
NOT_FOUND: object = object()
|
| 7 |
+
NULL: object = object()
|
| 8 |
+
_MAX_FIELD_SEARCH_OFFSET = 50
|
| 9 |
+
|
| 10 |
+
POINTER_N_BYTES = ctypes.sizeof(ctypes.c_void_p)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class DeduperReloaderPatchingMixin:
|
| 14 |
+
@staticmethod
|
| 15 |
+
def infer_field_offset(
|
| 16 |
+
obj: object,
|
| 17 |
+
field: str,
|
| 18 |
+
) -> int:
|
| 19 |
+
field_value = getattr(obj, field, NOT_FOUND)
|
| 20 |
+
if field_value is NOT_FOUND:
|
| 21 |
+
return -1
|
| 22 |
+
obj_addr = ctypes.c_void_p.from_buffer(ctypes.py_object(obj)).value
|
| 23 |
+
field_addr = ctypes.c_void_p.from_buffer(ctypes.py_object(field_value)).value
|
| 24 |
+
if obj_addr is None or field_addr is None:
|
| 25 |
+
return -1
|
| 26 |
+
ret = -1
|
| 27 |
+
for offset in range(1, _MAX_FIELD_SEARCH_OFFSET):
|
| 28 |
+
if (
|
| 29 |
+
ctypes.cast(
|
| 30 |
+
obj_addr + POINTER_N_BYTES * offset, ctypes.POINTER(ctypes.c_void_p)
|
| 31 |
+
).contents.value
|
| 32 |
+
== field_addr
|
| 33 |
+
):
|
| 34 |
+
ret = offset
|
| 35 |
+
break
|
| 36 |
+
return ret
|
| 37 |
+
|
| 38 |
+
@classmethod
|
| 39 |
+
def try_write_readonly_attr(
|
| 40 |
+
cls,
|
| 41 |
+
obj: object,
|
| 42 |
+
field: str,
|
| 43 |
+
new_value: object,
|
| 44 |
+
offset: int | None = None,
|
| 45 |
+
) -> None:
|
| 46 |
+
prev_value = getattr(obj, field, NOT_FOUND)
|
| 47 |
+
if prev_value is NOT_FOUND:
|
| 48 |
+
return
|
| 49 |
+
if offset is None:
|
| 50 |
+
offset = cls.infer_field_offset(obj, field)
|
| 51 |
+
if offset == -1:
|
| 52 |
+
return
|
| 53 |
+
obj_addr = ctypes.c_void_p.from_buffer(ctypes.py_object(obj)).value
|
| 54 |
+
if new_value is NULL:
|
| 55 |
+
new_value_addr: int | None = 0
|
| 56 |
+
else:
|
| 57 |
+
new_value_addr = ctypes.c_void_p.from_buffer(
|
| 58 |
+
ctypes.py_object(new_value)
|
| 59 |
+
).value
|
| 60 |
+
if obj_addr is None or new_value_addr is None:
|
| 61 |
+
return
|
| 62 |
+
if prev_value is not None:
|
| 63 |
+
ctypes.pythonapi.Py_DecRef(ctypes.py_object(prev_value))
|
| 64 |
+
if new_value not in (None, NULL):
|
| 65 |
+
ctypes.pythonapi.Py_IncRef(ctypes.py_object(new_value))
|
| 66 |
+
ctypes.cast(
|
| 67 |
+
obj_addr + POINTER_N_BYTES * offset, ctypes.POINTER(ctypes.c_void_p)
|
| 68 |
+
).contents.value = new_value_addr
|
| 69 |
+
|
| 70 |
+
@classmethod
|
| 71 |
+
def try_patch_readonly_attr(
|
| 72 |
+
cls,
|
| 73 |
+
old: object,
|
| 74 |
+
new: object,
|
| 75 |
+
field: str,
|
| 76 |
+
new_is_value: bool = False,
|
| 77 |
+
offset: int = -1,
|
| 78 |
+
) -> None:
|
| 79 |
+
old_value = getattr(old, field, NOT_FOUND)
|
| 80 |
+
new_value = new if new_is_value else getattr(new, field, NOT_FOUND)
|
| 81 |
+
if old_value is NOT_FOUND or new_value is NOT_FOUND:
|
| 82 |
+
return
|
| 83 |
+
elif old_value is new_value:
|
| 84 |
+
return
|
| 85 |
+
elif old_value is not None and offset < 0:
|
| 86 |
+
offset = cls.infer_field_offset(old, field)
|
| 87 |
+
elif offset < 0:
|
| 88 |
+
assert not new_is_value
|
| 89 |
+
assert new_value is not None
|
| 90 |
+
offset = cls.infer_field_offset(new, field)
|
| 91 |
+
cls.try_write_readonly_attr(old, field, new_value, offset=offset)
|
| 92 |
+
|
| 93 |
+
@classmethod
|
| 94 |
+
def try_patch_attr(
|
| 95 |
+
cls,
|
| 96 |
+
old: object,
|
| 97 |
+
new: object,
|
| 98 |
+
field: str,
|
| 99 |
+
new_is_value: bool = False,
|
| 100 |
+
offset: int = -1,
|
| 101 |
+
) -> None:
|
| 102 |
+
try:
|
| 103 |
+
setattr(old, field, new if new_is_value else getattr(new, field))
|
| 104 |
+
except (AttributeError, TypeError, ValueError):
|
| 105 |
+
cls.try_patch_readonly_attr(old, new, field, new_is_value, offset)
|
| 106 |
+
|
| 107 |
+
@classmethod
|
| 108 |
+
def patch_function(
|
| 109 |
+
cls, to_patch_to: Any, to_patch_from: Any, is_method: bool
|
| 110 |
+
) -> None:
|
| 111 |
+
new_closure = []
|
| 112 |
+
for freevar, closure_val in zip(
|
| 113 |
+
to_patch_from.__code__.co_freevars or [], to_patch_from.__closure__ or []
|
| 114 |
+
):
|
| 115 |
+
if (
|
| 116 |
+
callable(closure_val.cell_contents)
|
| 117 |
+
and freevar in to_patch_to.__code__.co_freevars
|
| 118 |
+
):
|
| 119 |
+
new_closure.append(
|
| 120 |
+
to_patch_to.__closure__[
|
| 121 |
+
to_patch_to.__code__.co_freevars.index(freevar)
|
| 122 |
+
]
|
| 123 |
+
)
|
| 124 |
+
else:
|
| 125 |
+
new_closure.append(closure_val)
|
| 126 |
+
# lambdas may complain if there is more than one freevar
|
| 127 |
+
cls.try_patch_attr(to_patch_to, to_patch_from, "__code__")
|
| 128 |
+
offset = -1
|
| 129 |
+
if to_patch_to.__closure__ is None and to_patch_from.__closure__ is not None:
|
| 130 |
+
offset = cls.infer_field_offset(to_patch_from, "__closure__")
|
| 131 |
+
if to_patch_to.__closure__ is not None or to_patch_from.__closure__ is not None:
|
| 132 |
+
cls.try_patch_readonly_attr(
|
| 133 |
+
to_patch_to,
|
| 134 |
+
tuple(new_closure) or NULL,
|
| 135 |
+
"__closure__",
|
| 136 |
+
new_is_value=True,
|
| 137 |
+
offset=offset,
|
| 138 |
+
)
|
| 139 |
+
for attr in ("__defaults__", "__kwdefaults__", "__doc__", "__dict__"):
|
| 140 |
+
cls.try_patch_attr(to_patch_to, to_patch_from, attr)
|
| 141 |
+
if is_method:
|
| 142 |
+
cls.try_patch_readonly_attr(to_patch_to, to_patch_from, "__self__")
|
lib/python3.12/site-packages/IPython/extensions/storemagic.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
%store magic for lightweight persistence.
|
| 4 |
+
|
| 5 |
+
Stores variables, aliases and macros in IPython's database.
|
| 6 |
+
|
| 7 |
+
To automatically restore stored variables at startup, add this to your
|
| 8 |
+
:file:`ipython_config.py` file::
|
| 9 |
+
|
| 10 |
+
c.StoreMagics.autorestore = True
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
# Copyright (c) IPython Development Team.
|
| 14 |
+
# Distributed under the terms of the Modified BSD License.
|
| 15 |
+
|
| 16 |
+
import inspect, os, sys, textwrap
|
| 17 |
+
|
| 18 |
+
from IPython.core.error import UsageError
|
| 19 |
+
from IPython.core.magic import Magics, magics_class, line_magic
|
| 20 |
+
from IPython.testing.skipdoctest import skip_doctest
|
| 21 |
+
from traitlets import Bool
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def restore_aliases(ip, alias=None):
|
| 25 |
+
staliases = ip.db.get('stored_aliases', {})
|
| 26 |
+
if alias is None:
|
| 27 |
+
for k,v in staliases.items():
|
| 28 |
+
# print("restore alias",k,v) # dbg
|
| 29 |
+
#self.alias_table[k] = v
|
| 30 |
+
ip.alias_manager.define_alias(k,v)
|
| 31 |
+
else:
|
| 32 |
+
ip.alias_manager.define_alias(alias, staliases[alias])
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def refresh_variables(ip):
|
| 36 |
+
db = ip.db
|
| 37 |
+
for key in db.keys('autorestore/*'):
|
| 38 |
+
# strip autorestore
|
| 39 |
+
justkey = os.path.basename(key)
|
| 40 |
+
try:
|
| 41 |
+
obj = db[key]
|
| 42 |
+
except KeyError:
|
| 43 |
+
print("Unable to restore variable '%s', ignoring (use %%store -d to forget!)" % justkey)
|
| 44 |
+
print("The error was:", sys.exc_info()[0])
|
| 45 |
+
else:
|
| 46 |
+
# print("restored",justkey,"=",obj) # dbg
|
| 47 |
+
ip.user_ns[justkey] = obj
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def restore_dhist(ip):
|
| 51 |
+
ip.user_ns['_dh'] = ip.db.get('dhist',[])
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def restore_data(ip):
|
| 55 |
+
refresh_variables(ip)
|
| 56 |
+
restore_aliases(ip)
|
| 57 |
+
restore_dhist(ip)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@magics_class
|
| 61 |
+
class StoreMagics(Magics):
|
| 62 |
+
"""Lightweight persistence for python variables.
|
| 63 |
+
|
| 64 |
+
Provides the %store magic."""
|
| 65 |
+
|
| 66 |
+
autorestore = Bool(False, help=
|
| 67 |
+
"""If True, any %store-d variables will be automatically restored
|
| 68 |
+
when IPython starts.
|
| 69 |
+
"""
|
| 70 |
+
).tag(config=True)
|
| 71 |
+
|
| 72 |
+
def __init__(self, shell):
|
| 73 |
+
super(StoreMagics, self).__init__(shell=shell)
|
| 74 |
+
self.shell.configurables.append(self)
|
| 75 |
+
if self.autorestore:
|
| 76 |
+
restore_data(self.shell)
|
| 77 |
+
|
| 78 |
+
@skip_doctest
|
| 79 |
+
@line_magic
|
| 80 |
+
def store(self, parameter_s=''):
|
| 81 |
+
"""Lightweight persistence for python variables.
|
| 82 |
+
|
| 83 |
+
Example::
|
| 84 |
+
|
| 85 |
+
In [1]: l = ['hello',10,'world']
|
| 86 |
+
In [2]: %store l
|
| 87 |
+
Stored 'l' (list)
|
| 88 |
+
In [3]: exit
|
| 89 |
+
|
| 90 |
+
(IPython session is closed and started again...)
|
| 91 |
+
|
| 92 |
+
ville@badger:~$ ipython
|
| 93 |
+
In [1]: l
|
| 94 |
+
NameError: name 'l' is not defined
|
| 95 |
+
In [2]: %store -r
|
| 96 |
+
In [3]: l
|
| 97 |
+
Out[3]: ['hello', 10, 'world']
|
| 98 |
+
|
| 99 |
+
Usage:
|
| 100 |
+
|
| 101 |
+
* ``%store`` - Show list of all variables and their current
|
| 102 |
+
values
|
| 103 |
+
* ``%store spam bar`` - Store the *current* value of the variables spam
|
| 104 |
+
and bar to disk
|
| 105 |
+
* ``%store -d spam`` - Remove the variable and its value from storage
|
| 106 |
+
* ``%store -z`` - Remove all variables from storage
|
| 107 |
+
* ``%store -r`` - Refresh all variables, aliases and directory history
|
| 108 |
+
from store (overwrite current vals)
|
| 109 |
+
* ``%store -r spam bar`` - Refresh specified variables and aliases from store
|
| 110 |
+
(delete current val)
|
| 111 |
+
* ``%store foo >a.txt`` - Store value of foo to new file a.txt
|
| 112 |
+
* ``%store foo >>a.txt`` - Append value of foo to file a.txt
|
| 113 |
+
|
| 114 |
+
It should be noted that if you change the value of a variable, you
|
| 115 |
+
need to %store it again if you want to persist the new value.
|
| 116 |
+
|
| 117 |
+
Note also that the variables will need to be pickleable; most basic
|
| 118 |
+
python types can be safely %store'd.
|
| 119 |
+
|
| 120 |
+
Also aliases can be %store'd across sessions.
|
| 121 |
+
To remove an alias from the storage, use the %unalias magic.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
opts,argsl = self.parse_options(parameter_s,'drz',mode='string')
|
| 125 |
+
args = argsl.split()
|
| 126 |
+
ip = self.shell
|
| 127 |
+
db = ip.db
|
| 128 |
+
# delete
|
| 129 |
+
if 'd' in opts:
|
| 130 |
+
try:
|
| 131 |
+
todel = args[0]
|
| 132 |
+
except IndexError as e:
|
| 133 |
+
raise UsageError('You must provide the variable to forget') from e
|
| 134 |
+
else:
|
| 135 |
+
try:
|
| 136 |
+
del db['autorestore/' + todel]
|
| 137 |
+
except BaseException as e:
|
| 138 |
+
raise UsageError("Can't delete variable '%s'" % todel) from e
|
| 139 |
+
# reset
|
| 140 |
+
elif 'z' in opts:
|
| 141 |
+
for k in db.keys('autorestore/*'):
|
| 142 |
+
del db[k]
|
| 143 |
+
|
| 144 |
+
elif 'r' in opts:
|
| 145 |
+
if args:
|
| 146 |
+
for arg in args:
|
| 147 |
+
try:
|
| 148 |
+
obj = db["autorestore/" + arg]
|
| 149 |
+
except KeyError:
|
| 150 |
+
try:
|
| 151 |
+
restore_aliases(ip, alias=arg)
|
| 152 |
+
except KeyError:
|
| 153 |
+
print("no stored variable or alias %s" % arg)
|
| 154 |
+
else:
|
| 155 |
+
ip.user_ns[arg] = obj
|
| 156 |
+
else:
|
| 157 |
+
restore_data(ip)
|
| 158 |
+
|
| 159 |
+
# run without arguments -> list variables & values
|
| 160 |
+
elif not args:
|
| 161 |
+
vars = db.keys('autorestore/*')
|
| 162 |
+
vars.sort()
|
| 163 |
+
if vars:
|
| 164 |
+
size = max(map(len, vars))
|
| 165 |
+
else:
|
| 166 |
+
size = 0
|
| 167 |
+
|
| 168 |
+
print('Stored variables and their in-db values:')
|
| 169 |
+
fmt = '%-'+str(size)+'s -> %s'
|
| 170 |
+
get = db.get
|
| 171 |
+
for var in vars:
|
| 172 |
+
justkey = os.path.basename(var)
|
| 173 |
+
# print 30 first characters from every var
|
| 174 |
+
print(fmt % (justkey, repr(get(var, '<unavailable>'))[:50]))
|
| 175 |
+
|
| 176 |
+
# default action - store the variable
|
| 177 |
+
else:
|
| 178 |
+
# %store foo >file.txt or >>file.txt
|
| 179 |
+
if len(args) > 1 and args[1].startswith(">"):
|
| 180 |
+
fnam = os.path.expanduser(args[1].lstrip(">").lstrip())
|
| 181 |
+
if args[1].startswith(">>"):
|
| 182 |
+
fil = open(fnam, "a", encoding="utf-8")
|
| 183 |
+
else:
|
| 184 |
+
fil = open(fnam, "w", encoding="utf-8")
|
| 185 |
+
with fil:
|
| 186 |
+
obj = ip.ev(args[0])
|
| 187 |
+
print("Writing '%s' (%s) to file '%s'." % (args[0],
|
| 188 |
+
obj.__class__.__name__, fnam))
|
| 189 |
+
|
| 190 |
+
if not isinstance (obj, str):
|
| 191 |
+
from pprint import pprint
|
| 192 |
+
pprint(obj, fil)
|
| 193 |
+
else:
|
| 194 |
+
fil.write(obj)
|
| 195 |
+
if not obj.endswith('\n'):
|
| 196 |
+
fil.write('\n')
|
| 197 |
+
|
| 198 |
+
return
|
| 199 |
+
|
| 200 |
+
# %store foo
|
| 201 |
+
for arg in args:
|
| 202 |
+
try:
|
| 203 |
+
obj = ip.user_ns[arg]
|
| 204 |
+
except KeyError:
|
| 205 |
+
# it might be an alias
|
| 206 |
+
name = arg
|
| 207 |
+
try:
|
| 208 |
+
cmd = ip.alias_manager.retrieve_alias(name)
|
| 209 |
+
except ValueError as e:
|
| 210 |
+
raise UsageError("Unknown variable '%s'" % name) from e
|
| 211 |
+
|
| 212 |
+
staliases = db.get('stored_aliases',{})
|
| 213 |
+
staliases[name] = cmd
|
| 214 |
+
db['stored_aliases'] = staliases
|
| 215 |
+
print("Alias stored: %s (%s)" % (name, cmd))
|
| 216 |
+
return
|
| 217 |
+
|
| 218 |
+
else:
|
| 219 |
+
modname = getattr(inspect.getmodule(obj), '__name__', '')
|
| 220 |
+
if modname == '__main__':
|
| 221 |
+
print(textwrap.dedent("""\
|
| 222 |
+
Warning:%s is %s
|
| 223 |
+
Proper storage of interactively declared classes (or instances
|
| 224 |
+
of those classes) is not possible! Only instances
|
| 225 |
+
of classes in real modules on file system can be %%store'd.
|
| 226 |
+
""" % (arg, obj) ))
|
| 227 |
+
return
|
| 228 |
+
#pickled = pickle.dumps(obj)
|
| 229 |
+
db[ 'autorestore/' + arg ] = obj
|
| 230 |
+
print("Stored '%s' (%s)" % (arg, obj.__class__.__name__))
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def load_ipython_extension(ip):
|
| 234 |
+
"""Load the extension in IPython."""
|
| 235 |
+
ip.register_magics(StoreMagics)
|
| 236 |
+
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (195 Bytes). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/debugger.cpython-312.pyc
ADDED
|
Binary file (9.07 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/embed.cpython-312.pyc
ADDED
|
Binary file (17.6 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/interactiveshell.cpython-312.pyc
ADDED
|
Binary file (47.3 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/ipapp.cpython-312.pyc
ADDED
|
Binary file (14.3 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/magics.cpython-312.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/prompts.cpython-312.pyc
ADDED
|
Binary file (8.14 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/__pycache__/ptutils.cpython-312.pyc
ADDED
|
Binary file (9.76 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (22.2 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_match.cpython-312.pyc
ADDED
|
Binary file (4.8 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/auto_suggest.cpython-312.pyc
ADDED
|
Binary file (30.2 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/shortcuts/__pycache__/filters.cpython-312.pyc
ADDED
|
Binary file (13.7 kB). View file
|
|
|
lib/python3.12/site-packages/IPython/terminal/shortcuts/auto_suggest.py
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import asyncio
|
| 3 |
+
import tokenize
|
| 4 |
+
from io import StringIO
|
| 5 |
+
from typing import List, Optional, Union, Tuple, ClassVar, Any
|
| 6 |
+
from collections.abc import Callable, Generator
|
| 7 |
+
import warnings
|
| 8 |
+
|
| 9 |
+
import prompt_toolkit
|
| 10 |
+
from prompt_toolkit.buffer import Buffer
|
| 11 |
+
from prompt_toolkit.key_binding import KeyPressEvent
|
| 12 |
+
from prompt_toolkit.key_binding.bindings import named_commands as nc
|
| 13 |
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
|
| 14 |
+
from prompt_toolkit.document import Document
|
| 15 |
+
from prompt_toolkit.history import History
|
| 16 |
+
from prompt_toolkit.shortcuts import PromptSession
|
| 17 |
+
from prompt_toolkit.layout.processors import (
|
| 18 |
+
Processor,
|
| 19 |
+
Transformation,
|
| 20 |
+
TransformationInput,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
from IPython.core.getipython import get_ipython
|
| 24 |
+
from IPython.utils.tokenutil import generate_tokens
|
| 25 |
+
|
| 26 |
+
from .filters import pass_through
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _get_query(document: Document):
|
| 30 |
+
return document.lines[document.cursor_position_row]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class AppendAutoSuggestionInAnyLine(Processor):
|
| 34 |
+
"""
|
| 35 |
+
Append the auto suggestion to lines other than the last (appending to the
|
| 36 |
+
last line is natively supported by the prompt toolkit).
|
| 37 |
+
|
| 38 |
+
This has a private `_debug` attribute that can be set to True to display
|
| 39 |
+
debug information as virtual suggestion on the end of any line. You can do
|
| 40 |
+
so with:
|
| 41 |
+
|
| 42 |
+
>>> from IPython.terminal.shortcuts.auto_suggest import AppendAutoSuggestionInAnyLine
|
| 43 |
+
>>> AppendAutoSuggestionInAnyLine._debug = True
|
| 44 |
+
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
_debug: ClassVar[bool] = False
|
| 48 |
+
|
| 49 |
+
def __init__(self, style: str = "class:auto-suggestion") -> None:
|
| 50 |
+
self.style = style
|
| 51 |
+
|
| 52 |
+
def apply_transformation(self, ti: TransformationInput) -> Transformation:
|
| 53 |
+
"""
|
| 54 |
+
Apply transformation to the line that is currently being edited.
|
| 55 |
+
|
| 56 |
+
This is a variation of the original implementation in prompt toolkit
|
| 57 |
+
that allows to not only append suggestions to any line, but also to show
|
| 58 |
+
multi-line suggestions.
|
| 59 |
+
|
| 60 |
+
As transformation are applied on a line-by-line basis; we need to trick
|
| 61 |
+
a bit, and elide any line that is after the line we are currently
|
| 62 |
+
editing, until we run out of completions. We cannot shift the existing
|
| 63 |
+
lines
|
| 64 |
+
|
| 65 |
+
There are multiple cases to handle:
|
| 66 |
+
|
| 67 |
+
The completions ends before the end of the buffer:
|
| 68 |
+
We can resume showing the normal line, and say that some code may
|
| 69 |
+
be hidden.
|
| 70 |
+
|
| 71 |
+
The completions ends at the end of the buffer
|
| 72 |
+
We can just say that some code may be hidden.
|
| 73 |
+
|
| 74 |
+
And separately:
|
| 75 |
+
|
| 76 |
+
The completions ends beyond the end of the buffer
|
| 77 |
+
We need to both say that some code may be hidden, and that some
|
| 78 |
+
lines are not shown.
|
| 79 |
+
|
| 80 |
+
"""
|
| 81 |
+
last_line_number = ti.document.line_count - 1
|
| 82 |
+
is_last_line = ti.lineno == last_line_number
|
| 83 |
+
|
| 84 |
+
noop = lambda text: Transformation(
|
| 85 |
+
fragments=ti.fragments + [(self.style, " " + text if self._debug else "")]
|
| 86 |
+
)
|
| 87 |
+
if ti.document.line_count == 1:
|
| 88 |
+
return noop("noop:oneline")
|
| 89 |
+
if ti.document.cursor_position_row == last_line_number and is_last_line:
|
| 90 |
+
# prompt toolkit already appends something; just leave it be
|
| 91 |
+
return noop("noop:last line and cursor")
|
| 92 |
+
|
| 93 |
+
# first everything before the current line is unchanged.
|
| 94 |
+
if ti.lineno < ti.document.cursor_position_row:
|
| 95 |
+
return noop("noop:before cursor")
|
| 96 |
+
|
| 97 |
+
buffer = ti.buffer_control.buffer
|
| 98 |
+
if not buffer.suggestion or not ti.document.is_cursor_at_the_end_of_line:
|
| 99 |
+
return noop("noop:not eol")
|
| 100 |
+
|
| 101 |
+
delta = ti.lineno - ti.document.cursor_position_row
|
| 102 |
+
suggestions = buffer.suggestion.text.splitlines()
|
| 103 |
+
|
| 104 |
+
if len(suggestions) == 0:
|
| 105 |
+
return noop("noop: no suggestions")
|
| 106 |
+
|
| 107 |
+
if prompt_toolkit.VERSION < (3, 0, 49):
|
| 108 |
+
if len(suggestions) > 1 and prompt_toolkit.VERSION < (3, 0, 49):
|
| 109 |
+
if ti.lineno == ti.document.cursor_position_row:
|
| 110 |
+
return Transformation(
|
| 111 |
+
fragments=ti.fragments
|
| 112 |
+
+ [
|
| 113 |
+
(
|
| 114 |
+
"red",
|
| 115 |
+
"(Cannot show multiline suggestion; requires prompt_toolkit > 3.0.49)",
|
| 116 |
+
)
|
| 117 |
+
]
|
| 118 |
+
)
|
| 119 |
+
else:
|
| 120 |
+
return Transformation(fragments=ti.fragments)
|
| 121 |
+
elif len(suggestions) == 1:
|
| 122 |
+
if ti.lineno == ti.document.cursor_position_row:
|
| 123 |
+
return Transformation(
|
| 124 |
+
fragments=ti.fragments + [(self.style, suggestions[0])]
|
| 125 |
+
)
|
| 126 |
+
return Transformation(fragments=ti.fragments)
|
| 127 |
+
|
| 128 |
+
if delta == 0:
|
| 129 |
+
suggestion = suggestions[0]
|
| 130 |
+
return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
|
| 131 |
+
if is_last_line:
|
| 132 |
+
if delta < len(suggestions):
|
| 133 |
+
suggestion = f"… rest of suggestion ({len(suggestions) - delta} lines) and code hidden"
|
| 134 |
+
return Transformation([(self.style, suggestion)])
|
| 135 |
+
|
| 136 |
+
n_elided = len(suggestions)
|
| 137 |
+
for i in range(len(suggestions)):
|
| 138 |
+
ll = ti.get_line(last_line_number - i)
|
| 139 |
+
el = "".join(l[1] for l in ll).strip()
|
| 140 |
+
if el:
|
| 141 |
+
break
|
| 142 |
+
else:
|
| 143 |
+
n_elided -= 1
|
| 144 |
+
if n_elided:
|
| 145 |
+
return Transformation([(self.style, f"… {n_elided} line(s) hidden")])
|
| 146 |
+
else:
|
| 147 |
+
return Transformation(
|
| 148 |
+
ti.get_line(last_line_number - len(suggestions) + 1)
|
| 149 |
+
+ ([(self.style, "shift-last-line")] if self._debug else [])
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
elif delta < len(suggestions):
|
| 153 |
+
suggestion = suggestions[delta]
|
| 154 |
+
return Transformation([(self.style, suggestion)])
|
| 155 |
+
else:
|
| 156 |
+
shift = ti.lineno - len(suggestions) + 1
|
| 157 |
+
return Transformation(ti.get_line(shift))
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
|
| 161 |
+
"""
|
| 162 |
+
A subclass of AutoSuggestFromHistory that allow navigation to next/previous
|
| 163 |
+
suggestion from history. To do so it remembers the current position, but it
|
| 164 |
+
state need to carefully be cleared on the right events.
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
skip_lines: int
|
| 168 |
+
_connected_apps: list[PromptSession]
|
| 169 |
+
|
| 170 |
+
# handle to the currently running llm task that appends suggestions to the
|
| 171 |
+
# current buffer; we keep a handle to it in order to cancel it when there is a cursor movement, or
|
| 172 |
+
# another request.
|
| 173 |
+
_llm_task: asyncio.Task | None = None
|
| 174 |
+
|
| 175 |
+
# This is the constructor of the LLM provider from jupyter-ai
|
| 176 |
+
# to which we forward the request to generate inline completions.
|
| 177 |
+
_init_llm_provider: Callable | None
|
| 178 |
+
|
| 179 |
+
_llm_provider_instance: Any | None
|
| 180 |
+
_llm_prefixer: Callable = lambda self, x: "wrong"
|
| 181 |
+
|
| 182 |
+
def __init__(self):
|
| 183 |
+
super().__init__()
|
| 184 |
+
self.skip_lines = 0
|
| 185 |
+
self._connected_apps = []
|
| 186 |
+
self._llm_provider_instance = None
|
| 187 |
+
self._init_llm_provider = None
|
| 188 |
+
self._request_number = 0
|
| 189 |
+
|
| 190 |
+
def reset_history_position(self, _: Buffer) -> None:
|
| 191 |
+
self.skip_lines = 0
|
| 192 |
+
|
| 193 |
+
def disconnect(self) -> None:
|
| 194 |
+
self._cancel_running_llm_task()
|
| 195 |
+
for pt_app in self._connected_apps:
|
| 196 |
+
text_insert_event = pt_app.default_buffer.on_text_insert
|
| 197 |
+
text_insert_event.remove_handler(self.reset_history_position)
|
| 198 |
+
|
| 199 |
+
def connect(self, pt_app: PromptSession) -> None:
|
| 200 |
+
self._connected_apps.append(pt_app)
|
| 201 |
+
# note: `on_text_changed` could be used for a bit different behaviour
|
| 202 |
+
# on character deletion (i.e. resetting history position on backspace)
|
| 203 |
+
pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
|
| 204 |
+
pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
|
| 205 |
+
|
| 206 |
+
def get_suggestion(
|
| 207 |
+
self, buffer: Buffer, document: Document
|
| 208 |
+
) -> Optional[Suggestion]:
|
| 209 |
+
text = _get_query(document)
|
| 210 |
+
|
| 211 |
+
if text.strip():
|
| 212 |
+
for suggestion, _ in self._find_next_match(
|
| 213 |
+
text, self.skip_lines, buffer.history
|
| 214 |
+
):
|
| 215 |
+
return Suggestion(suggestion)
|
| 216 |
+
|
| 217 |
+
return None
|
| 218 |
+
|
| 219 |
+
def _dismiss(self, buffer, *args, **kwargs) -> None:
|
| 220 |
+
self._cancel_running_llm_task()
|
| 221 |
+
buffer.suggestion = None
|
| 222 |
+
|
| 223 |
+
def _find_match(
|
| 224 |
+
self, text: str, skip_lines: float, history: History, previous: bool
|
| 225 |
+
) -> Generator[Tuple[str, float], None, None]:
|
| 226 |
+
"""
|
| 227 |
+
text : str
|
| 228 |
+
Text content to find a match for, the user cursor is most of the
|
| 229 |
+
time at the end of this text.
|
| 230 |
+
skip_lines : float
|
| 231 |
+
number of items to skip in the search, this is used to indicate how
|
| 232 |
+
far in the list the user has navigated by pressing up or down.
|
| 233 |
+
The float type is used as the base value is +inf
|
| 234 |
+
history : History
|
| 235 |
+
prompt_toolkit History instance to fetch previous entries from.
|
| 236 |
+
previous : bool
|
| 237 |
+
Direction of the search, whether we are looking previous match
|
| 238 |
+
(True), or next match (False).
|
| 239 |
+
|
| 240 |
+
Yields
|
| 241 |
+
------
|
| 242 |
+
Tuple with:
|
| 243 |
+
str:
|
| 244 |
+
current suggestion.
|
| 245 |
+
float:
|
| 246 |
+
will actually yield only ints, which is passed back via skip_lines,
|
| 247 |
+
which may be a +inf (float)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
"""
|
| 251 |
+
line_number = -1
|
| 252 |
+
for string in reversed(list(history.get_strings())):
|
| 253 |
+
for line in reversed(string.splitlines()):
|
| 254 |
+
line_number += 1
|
| 255 |
+
if not previous and line_number < skip_lines:
|
| 256 |
+
continue
|
| 257 |
+
# do not return empty suggestions as these
|
| 258 |
+
# close the auto-suggestion overlay (and are useless)
|
| 259 |
+
if line.startswith(text) and len(line) > len(text):
|
| 260 |
+
yield line[len(text) :], line_number
|
| 261 |
+
if previous and line_number >= skip_lines:
|
| 262 |
+
return
|
| 263 |
+
|
| 264 |
+
def _find_next_match(
|
| 265 |
+
self, text: str, skip_lines: float, history: History
|
| 266 |
+
) -> Generator[Tuple[str, float], None, None]:
|
| 267 |
+
return self._find_match(text, skip_lines, history, previous=False)
|
| 268 |
+
|
| 269 |
+
def _find_previous_match(self, text: str, skip_lines: float, history: History):
|
| 270 |
+
return reversed(
|
| 271 |
+
list(self._find_match(text, skip_lines, history, previous=True))
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
def up(self, query: str, other_than: str, history: History) -> None:
|
| 275 |
+
self._cancel_running_llm_task()
|
| 276 |
+
for suggestion, line_number in self._find_next_match(
|
| 277 |
+
query, self.skip_lines, history
|
| 278 |
+
):
|
| 279 |
+
# if user has history ['very.a', 'very', 'very.b'] and typed 'very'
|
| 280 |
+
# we want to switch from 'very.b' to 'very.a' because a) if the
|
| 281 |
+
# suggestion equals current text, prompt-toolkit aborts suggesting
|
| 282 |
+
# b) user likely would not be interested in 'very' anyways (they
|
| 283 |
+
# already typed it).
|
| 284 |
+
if query + suggestion != other_than:
|
| 285 |
+
self.skip_lines = line_number
|
| 286 |
+
break
|
| 287 |
+
else:
|
| 288 |
+
# no matches found, cycle back to beginning
|
| 289 |
+
self.skip_lines = 0
|
| 290 |
+
|
| 291 |
+
def down(self, query: str, other_than: str, history: History) -> None:
|
| 292 |
+
self._cancel_running_llm_task()
|
| 293 |
+
for suggestion, line_number in self._find_previous_match(
|
| 294 |
+
query, self.skip_lines, history
|
| 295 |
+
):
|
| 296 |
+
if query + suggestion != other_than:
|
| 297 |
+
self.skip_lines = line_number
|
| 298 |
+
break
|
| 299 |
+
else:
|
| 300 |
+
# no matches found, cycle to end
|
| 301 |
+
for suggestion, line_number in self._find_previous_match(
|
| 302 |
+
query, float("Inf"), history
|
| 303 |
+
):
|
| 304 |
+
if query + suggestion != other_than:
|
| 305 |
+
self.skip_lines = line_number
|
| 306 |
+
break
|
| 307 |
+
|
| 308 |
+
def _cancel_running_llm_task(self) -> None:
|
| 309 |
+
"""
|
| 310 |
+
Try to cancel the currently running llm_task if exists, and set it to None.
|
| 311 |
+
"""
|
| 312 |
+
if self._llm_task is not None:
|
| 313 |
+
if self._llm_task.done():
|
| 314 |
+
self._llm_task = None
|
| 315 |
+
return
|
| 316 |
+
cancelled = self._llm_task.cancel()
|
| 317 |
+
if cancelled:
|
| 318 |
+
self._llm_task = None
|
| 319 |
+
if not cancelled:
|
| 320 |
+
warnings.warn(
|
| 321 |
+
"LLM task not cancelled, does your provider support cancellation?"
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
@property
|
| 325 |
+
def _llm_provider(self):
|
| 326 |
+
"""Lazy-initialized instance of the LLM provider.
|
| 327 |
+
|
| 328 |
+
Do not use in the constructor, as `_init_llm_provider` can trigger slow side-effects.
|
| 329 |
+
"""
|
| 330 |
+
if self._llm_provider_instance is None and self._init_llm_provider:
|
| 331 |
+
self._llm_provider_instance = self._init_llm_provider()
|
| 332 |
+
return self._llm_provider_instance
|
| 333 |
+
|
| 334 |
+
async def _trigger_llm(self, buffer) -> None:
|
| 335 |
+
"""
|
| 336 |
+
This will ask the current llm provider a suggestion for the current buffer.
|
| 337 |
+
|
| 338 |
+
If there is a currently running llm task, it will cancel it.
|
| 339 |
+
"""
|
| 340 |
+
# we likely want to store the current cursor position, and cancel if the cursor has moved.
|
| 341 |
+
try:
|
| 342 |
+
import jupyter_ai_magics
|
| 343 |
+
except ModuleNotFoundError:
|
| 344 |
+
jupyter_ai_magics = None
|
| 345 |
+
if not self._llm_provider:
|
| 346 |
+
warnings.warn("No LLM provider found, cannot trigger LLM completions")
|
| 347 |
+
return
|
| 348 |
+
if jupyter_ai_magics is None:
|
| 349 |
+
warnings.warn("LLM Completion requires `jupyter_ai_magics` to be installed")
|
| 350 |
+
|
| 351 |
+
self._cancel_running_llm_task()
|
| 352 |
+
|
| 353 |
+
async def error_catcher(buffer):
|
| 354 |
+
"""
|
| 355 |
+
This catches and log any errors, as otherwise this is just
|
| 356 |
+
lost in the void of the future running task.
|
| 357 |
+
"""
|
| 358 |
+
try:
|
| 359 |
+
await self._trigger_llm_core(buffer)
|
| 360 |
+
except Exception as e:
|
| 361 |
+
get_ipython().log.error("error %s", e)
|
| 362 |
+
raise
|
| 363 |
+
|
| 364 |
+
# here we need a cancellable task so we can't just await the error caught
|
| 365 |
+
self._llm_task = asyncio.create_task(error_catcher(buffer))
|
| 366 |
+
await self._llm_task
|
| 367 |
+
|
| 368 |
+
async def _trigger_llm_core(self, buffer: Buffer):
|
| 369 |
+
"""
|
| 370 |
+
This is the core of the current llm request.
|
| 371 |
+
|
| 372 |
+
Here we build a compatible `InlineCompletionRequest` and ask the llm
|
| 373 |
+
provider to stream it's response back to us iteratively setting it as
|
| 374 |
+
the suggestion on the current buffer.
|
| 375 |
+
|
| 376 |
+
Unlike with JupyterAi, as we do not have multiple cells, the cell id
|
| 377 |
+
is always set to `None`.
|
| 378 |
+
|
| 379 |
+
We set the prefix to the current cell content, but could also insert the
|
| 380 |
+
rest of the history or even just the non-fail history.
|
| 381 |
+
|
| 382 |
+
In the same way, we do not have cell id.
|
| 383 |
+
|
| 384 |
+
LLM provider may return multiple suggestion stream, but for the time
|
| 385 |
+
being we only support one.
|
| 386 |
+
|
| 387 |
+
Here we make the assumption that the provider will have
|
| 388 |
+
stream_inline_completions, I'm not sure it is the case for all
|
| 389 |
+
providers.
|
| 390 |
+
"""
|
| 391 |
+
try:
|
| 392 |
+
import jupyter_ai.completions.models as jai_models
|
| 393 |
+
except ModuleNotFoundError:
|
| 394 |
+
jai_models = None
|
| 395 |
+
|
| 396 |
+
if not jai_models:
|
| 397 |
+
raise ValueError("jupyter-ai is not installed")
|
| 398 |
+
|
| 399 |
+
if not self._llm_provider:
|
| 400 |
+
raise ValueError("No LLM provider found, cannot trigger LLM completions")
|
| 401 |
+
|
| 402 |
+
hm = buffer.history.shell.history_manager
|
| 403 |
+
prefix = self._llm_prefixer(hm)
|
| 404 |
+
get_ipython().log.debug("prefix: %s", prefix)
|
| 405 |
+
|
| 406 |
+
self._request_number += 1
|
| 407 |
+
request_number = self._request_number
|
| 408 |
+
|
| 409 |
+
request = jai_models.InlineCompletionRequest(
|
| 410 |
+
number=request_number,
|
| 411 |
+
prefix=prefix + buffer.document.text_before_cursor,
|
| 412 |
+
suffix=buffer.document.text_after_cursor,
|
| 413 |
+
mime="text/x-python",
|
| 414 |
+
stream=True,
|
| 415 |
+
path=None,
|
| 416 |
+
language="python",
|
| 417 |
+
cell_id=None,
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
async for reply_and_chunks in self._llm_provider.stream_inline_completions(
|
| 421 |
+
request
|
| 422 |
+
):
|
| 423 |
+
if self._request_number != request_number:
|
| 424 |
+
# If a new suggestion was requested, skip processing this one.
|
| 425 |
+
return
|
| 426 |
+
if isinstance(reply_and_chunks, jai_models.InlineCompletionReply):
|
| 427 |
+
if len(reply_and_chunks.list.items) > 1:
|
| 428 |
+
raise ValueError(
|
| 429 |
+
"Terminal IPython cannot deal with multiple LLM suggestions at once"
|
| 430 |
+
)
|
| 431 |
+
buffer.suggestion = Suggestion(
|
| 432 |
+
reply_and_chunks.list.items[0].insertText
|
| 433 |
+
)
|
| 434 |
+
buffer.on_suggestion_set.fire()
|
| 435 |
+
elif isinstance(reply_and_chunks, jai_models.InlineCompletionStreamChunk):
|
| 436 |
+
buffer.suggestion = Suggestion(reply_and_chunks.response.insertText)
|
| 437 |
+
buffer.on_suggestion_set.fire()
|
| 438 |
+
return
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
async def llm_autosuggestion(event: KeyPressEvent):
|
| 442 |
+
"""
|
| 443 |
+
Ask the AutoSuggester from history to delegate to ask an LLM for completion
|
| 444 |
+
|
| 445 |
+
This will first make sure that the current buffer have _MIN_LINES (7)
|
| 446 |
+
available lines to insert the LLM completion
|
| 447 |
+
|
| 448 |
+
Provisional as of 8.32, may change without warnings
|
| 449 |
+
|
| 450 |
+
"""
|
| 451 |
+
_MIN_LINES = 5
|
| 452 |
+
provider = get_ipython().auto_suggest
|
| 453 |
+
if not isinstance(provider, NavigableAutoSuggestFromHistory):
|
| 454 |
+
return
|
| 455 |
+
doc = event.current_buffer.document
|
| 456 |
+
lines_to_insert = max(0, _MIN_LINES - doc.line_count + doc.cursor_position_row)
|
| 457 |
+
for _ in range(lines_to_insert):
|
| 458 |
+
event.current_buffer.insert_text("\n", move_cursor=False, fire_event=False)
|
| 459 |
+
|
| 460 |
+
await provider._trigger_llm(event.current_buffer)
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def accept_or_jump_to_end(event: KeyPressEvent):
|
| 464 |
+
"""Apply autosuggestion or jump to end of line."""
|
| 465 |
+
buffer = event.current_buffer
|
| 466 |
+
d = buffer.document
|
| 467 |
+
after_cursor = d.text[d.cursor_position :]
|
| 468 |
+
lines = after_cursor.split("\n")
|
| 469 |
+
end_of_current_line = lines[0].strip()
|
| 470 |
+
suggestion = buffer.suggestion
|
| 471 |
+
if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
|
| 472 |
+
buffer.insert_text(suggestion.text)
|
| 473 |
+
else:
|
| 474 |
+
nc.end_of_line(event)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def accept(event: KeyPressEvent):
|
| 478 |
+
"""Accept autosuggestion"""
|
| 479 |
+
buffer = event.current_buffer
|
| 480 |
+
suggestion = buffer.suggestion
|
| 481 |
+
if suggestion:
|
| 482 |
+
buffer.insert_text(suggestion.text)
|
| 483 |
+
else:
|
| 484 |
+
nc.forward_char(event)
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def discard(event: KeyPressEvent):
|
| 488 |
+
"""Discard autosuggestion"""
|
| 489 |
+
buffer = event.current_buffer
|
| 490 |
+
buffer.suggestion = None
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def accept_word(event: KeyPressEvent):
|
| 494 |
+
"""Fill partial autosuggestion by word"""
|
| 495 |
+
buffer = event.current_buffer
|
| 496 |
+
suggestion = buffer.suggestion
|
| 497 |
+
if suggestion:
|
| 498 |
+
t = re.split(r"(\S+\s+)", suggestion.text)
|
| 499 |
+
buffer.insert_text(next((x for x in t if x), ""))
|
| 500 |
+
else:
|
| 501 |
+
nc.forward_word(event)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def accept_character(event: KeyPressEvent):
|
| 505 |
+
"""Fill partial autosuggestion by character"""
|
| 506 |
+
b = event.current_buffer
|
| 507 |
+
suggestion = b.suggestion
|
| 508 |
+
if suggestion and suggestion.text:
|
| 509 |
+
b.insert_text(suggestion.text[0])
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def accept_and_keep_cursor(event: KeyPressEvent):
|
| 513 |
+
"""Accept autosuggestion and keep cursor in place"""
|
| 514 |
+
buffer = event.current_buffer
|
| 515 |
+
old_position = buffer.cursor_position
|
| 516 |
+
suggestion = buffer.suggestion
|
| 517 |
+
if suggestion:
|
| 518 |
+
buffer.insert_text(suggestion.text)
|
| 519 |
+
buffer.cursor_position = old_position
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def accept_and_move_cursor_left(event: KeyPressEvent):
|
| 523 |
+
"""Accept autosuggestion and move cursor left in place"""
|
| 524 |
+
accept_and_keep_cursor(event)
|
| 525 |
+
nc.backward_char(event)
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def _update_hint(buffer: Buffer):
|
| 529 |
+
if buffer.auto_suggest:
|
| 530 |
+
suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
|
| 531 |
+
buffer.suggestion = suggestion
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
def backspace_and_resume_hint(event: KeyPressEvent):
|
| 535 |
+
"""Resume autosuggestions after deleting last character"""
|
| 536 |
+
nc.backward_delete_char(event)
|
| 537 |
+
_update_hint(event.current_buffer)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def resume_hinting(event: KeyPressEvent):
|
| 541 |
+
"""Resume autosuggestions"""
|
| 542 |
+
pass_through.reply(event)
|
| 543 |
+
# Order matters: if update happened first and event reply second, the
|
| 544 |
+
# suggestion would be auto-accepted if both actions are bound to same key.
|
| 545 |
+
_update_hint(event.current_buffer)
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
def up_and_update_hint(event: KeyPressEvent):
|
| 549 |
+
"""Go up and update hint"""
|
| 550 |
+
current_buffer = event.current_buffer
|
| 551 |
+
|
| 552 |
+
current_buffer.auto_up(count=event.arg)
|
| 553 |
+
_update_hint(current_buffer)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def down_and_update_hint(event: KeyPressEvent):
|
| 557 |
+
"""Go down and update hint"""
|
| 558 |
+
current_buffer = event.current_buffer
|
| 559 |
+
|
| 560 |
+
current_buffer.auto_down(count=event.arg)
|
| 561 |
+
_update_hint(current_buffer)
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
def accept_token(event: KeyPressEvent):
|
| 565 |
+
"""Fill partial autosuggestion by token"""
|
| 566 |
+
b = event.current_buffer
|
| 567 |
+
suggestion = b.suggestion
|
| 568 |
+
|
| 569 |
+
if suggestion:
|
| 570 |
+
prefix = _get_query(b.document)
|
| 571 |
+
text = prefix + suggestion.text
|
| 572 |
+
|
| 573 |
+
tokens: List[Optional[str]] = [None, None, None]
|
| 574 |
+
substrings = [""]
|
| 575 |
+
i = 0
|
| 576 |
+
|
| 577 |
+
for token in generate_tokens(StringIO(text).readline):
|
| 578 |
+
if token.type == tokenize.NEWLINE:
|
| 579 |
+
index = len(text)
|
| 580 |
+
else:
|
| 581 |
+
index = text.index(token[1], len(substrings[-1]))
|
| 582 |
+
substrings.append(text[:index])
|
| 583 |
+
tokenized_so_far = substrings[-1]
|
| 584 |
+
if tokenized_so_far.startswith(prefix):
|
| 585 |
+
if i == 0 and len(tokenized_so_far) > len(prefix):
|
| 586 |
+
tokens[0] = tokenized_so_far[len(prefix) :]
|
| 587 |
+
substrings.append(tokenized_so_far)
|
| 588 |
+
i += 1
|
| 589 |
+
tokens[i] = token[1]
|
| 590 |
+
if i == 2:
|
| 591 |
+
break
|
| 592 |
+
i += 1
|
| 593 |
+
|
| 594 |
+
if tokens[0]:
|
| 595 |
+
to_insert: str
|
| 596 |
+
insert_text = substrings[-2]
|
| 597 |
+
if tokens[1] and len(tokens[1]) == 1:
|
| 598 |
+
insert_text = substrings[-1]
|
| 599 |
+
to_insert = insert_text[len(prefix) :]
|
| 600 |
+
b.insert_text(to_insert)
|
| 601 |
+
return
|
| 602 |
+
|
| 603 |
+
nc.forward_word(event)
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
def _swap_autosuggestion(
|
| 610 |
+
buffer: Buffer,
|
| 611 |
+
provider: NavigableAutoSuggestFromHistory,
|
| 612 |
+
direction_method: Callable,
|
| 613 |
+
):
|
| 614 |
+
"""
|
| 615 |
+
We skip most recent history entry (in either direction) if it equals the
|
| 616 |
+
current autosuggestion because if user cycles when auto-suggestion is shown
|
| 617 |
+
they most likely want something else than what was suggested (otherwise
|
| 618 |
+
they would have accepted the suggestion).
|
| 619 |
+
"""
|
| 620 |
+
suggestion = buffer.suggestion
|
| 621 |
+
if not suggestion:
|
| 622 |
+
return
|
| 623 |
+
|
| 624 |
+
query = _get_query(buffer.document)
|
| 625 |
+
current = query + suggestion.text
|
| 626 |
+
|
| 627 |
+
direction_method(query=query, other_than=current, history=buffer.history)
|
| 628 |
+
|
| 629 |
+
new_suggestion = provider.get_suggestion(buffer, buffer.document)
|
| 630 |
+
buffer.suggestion = new_suggestion
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
def swap_autosuggestion_up(event: KeyPressEvent):
|
| 634 |
+
"""Get next autosuggestion from history."""
|
| 635 |
+
shell = get_ipython()
|
| 636 |
+
provider = shell.auto_suggest
|
| 637 |
+
|
| 638 |
+
if not isinstance(provider, NavigableAutoSuggestFromHistory):
|
| 639 |
+
return
|
| 640 |
+
|
| 641 |
+
return _swap_autosuggestion(
|
| 642 |
+
buffer=event.current_buffer, provider=provider, direction_method=provider.up
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
def swap_autosuggestion_down(event: KeyPressEvent):
|
| 647 |
+
"""Get previous autosuggestion from history."""
|
| 648 |
+
shell = get_ipython()
|
| 649 |
+
provider = shell.auto_suggest
|
| 650 |
+
|
| 651 |
+
if not isinstance(provider, NavigableAutoSuggestFromHistory):
|
| 652 |
+
return
|
| 653 |
+
|
| 654 |
+
return _swap_autosuggestion(
|
| 655 |
+
buffer=event.current_buffer,
|
| 656 |
+
provider=provider,
|
| 657 |
+
direction_method=provider.down,
|
| 658 |
+
)
|
lib/python3.12/site-packages/attrs/__init__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: MIT
|
| 2 |
+
|
| 3 |
+
from attr import (
|
| 4 |
+
NOTHING,
|
| 5 |
+
Attribute,
|
| 6 |
+
AttrsInstance,
|
| 7 |
+
Converter,
|
| 8 |
+
Factory,
|
| 9 |
+
NothingType,
|
| 10 |
+
_make_getattr,
|
| 11 |
+
assoc,
|
| 12 |
+
cmp_using,
|
| 13 |
+
define,
|
| 14 |
+
evolve,
|
| 15 |
+
field,
|
| 16 |
+
fields,
|
| 17 |
+
fields_dict,
|
| 18 |
+
frozen,
|
| 19 |
+
has,
|
| 20 |
+
make_class,
|
| 21 |
+
mutable,
|
| 22 |
+
resolve_types,
|
| 23 |
+
validate,
|
| 24 |
+
)
|
| 25 |
+
from attr._make import ClassProps
|
| 26 |
+
from attr._next_gen import asdict, astuple, inspect
|
| 27 |
+
|
| 28 |
+
from . import converters, exceptions, filters, setters, validators
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
__all__ = [
|
| 32 |
+
"NOTHING",
|
| 33 |
+
"Attribute",
|
| 34 |
+
"AttrsInstance",
|
| 35 |
+
"ClassProps",
|
| 36 |
+
"Converter",
|
| 37 |
+
"Factory",
|
| 38 |
+
"NothingType",
|
| 39 |
+
"__author__",
|
| 40 |
+
"__copyright__",
|
| 41 |
+
"__description__",
|
| 42 |
+
"__doc__",
|
| 43 |
+
"__email__",
|
| 44 |
+
"__license__",
|
| 45 |
+
"__title__",
|
| 46 |
+
"__url__",
|
| 47 |
+
"__version__",
|
| 48 |
+
"__version_info__",
|
| 49 |
+
"asdict",
|
| 50 |
+
"assoc",
|
| 51 |
+
"astuple",
|
| 52 |
+
"cmp_using",
|
| 53 |
+
"converters",
|
| 54 |
+
"define",
|
| 55 |
+
"evolve",
|
| 56 |
+
"exceptions",
|
| 57 |
+
"field",
|
| 58 |
+
"fields",
|
| 59 |
+
"fields_dict",
|
| 60 |
+
"filters",
|
| 61 |
+
"frozen",
|
| 62 |
+
"has",
|
| 63 |
+
"inspect",
|
| 64 |
+
"make_class",
|
| 65 |
+
"mutable",
|
| 66 |
+
"resolve_types",
|
| 67 |
+
"setters",
|
| 68 |
+
"validate",
|
| 69 |
+
"validators",
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
__getattr__ = _make_getattr(__name__)
|
lib/python3.12/site-packages/attrs/__init__.pyi
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
from typing import (
|
| 4 |
+
Any,
|
| 5 |
+
Callable,
|
| 6 |
+
Mapping,
|
| 7 |
+
Sequence,
|
| 8 |
+
overload,
|
| 9 |
+
TypeVar,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
# Because we need to type our own stuff, we have to make everything from
|
| 13 |
+
# attr explicitly public too.
|
| 14 |
+
from attr import __author__ as __author__
|
| 15 |
+
from attr import __copyright__ as __copyright__
|
| 16 |
+
from attr import __description__ as __description__
|
| 17 |
+
from attr import __email__ as __email__
|
| 18 |
+
from attr import __license__ as __license__
|
| 19 |
+
from attr import __title__ as __title__
|
| 20 |
+
from attr import __url__ as __url__
|
| 21 |
+
from attr import __version__ as __version__
|
| 22 |
+
from attr import __version_info__ as __version_info__
|
| 23 |
+
from attr import assoc as assoc
|
| 24 |
+
from attr import Attribute as Attribute
|
| 25 |
+
from attr import AttrsInstance as AttrsInstance
|
| 26 |
+
from attr import cmp_using as cmp_using
|
| 27 |
+
from attr import converters as converters
|
| 28 |
+
from attr import Converter as Converter
|
| 29 |
+
from attr import evolve as evolve
|
| 30 |
+
from attr import exceptions as exceptions
|
| 31 |
+
from attr import Factory as Factory
|
| 32 |
+
from attr import fields as fields
|
| 33 |
+
from attr import fields_dict as fields_dict
|
| 34 |
+
from attr import filters as filters
|
| 35 |
+
from attr import has as has
|
| 36 |
+
from attr import make_class as make_class
|
| 37 |
+
from attr import NOTHING as NOTHING
|
| 38 |
+
from attr import resolve_types as resolve_types
|
| 39 |
+
from attr import setters as setters
|
| 40 |
+
from attr import validate as validate
|
| 41 |
+
from attr import validators as validators
|
| 42 |
+
from attr import attrib, asdict as asdict, astuple as astuple
|
| 43 |
+
from attr import NothingType as NothingType
|
| 44 |
+
|
| 45 |
+
if sys.version_info >= (3, 11):
|
| 46 |
+
from typing import dataclass_transform
|
| 47 |
+
else:
|
| 48 |
+
from typing_extensions import dataclass_transform
|
| 49 |
+
|
| 50 |
+
_T = TypeVar("_T")
|
| 51 |
+
_C = TypeVar("_C", bound=type)
|
| 52 |
+
|
| 53 |
+
_EqOrderType = bool | Callable[[Any], Any]
|
| 54 |
+
_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any]
|
| 55 |
+
_CallableConverterType = Callable[[Any], Any]
|
| 56 |
+
_ConverterType = _CallableConverterType | Converter[Any, Any]
|
| 57 |
+
_ReprType = Callable[[Any], str]
|
| 58 |
+
_ReprArgType = bool | _ReprType
|
| 59 |
+
_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any]
|
| 60 |
+
_OnSetAttrArgType = _OnSetAttrType | list[_OnSetAttrType] | setters._NoOpType
|
| 61 |
+
_FieldTransformer = Callable[
|
| 62 |
+
[type, list["Attribute[Any]"]], list["Attribute[Any]"]
|
| 63 |
+
]
|
| 64 |
+
# FIXME: in reality, if multiple validators are passed they must be in a list
|
| 65 |
+
# or tuple, but those are invariant and so would prevent subtypes of
|
| 66 |
+
# _ValidatorType from working when passed in a list or tuple.
|
| 67 |
+
_ValidatorArgType = _ValidatorType[_T] | Sequence[_ValidatorType[_T]]
|
| 68 |
+
|
| 69 |
+
@overload
|
| 70 |
+
def field(
|
| 71 |
+
*,
|
| 72 |
+
default: None = ...,
|
| 73 |
+
validator: None = ...,
|
| 74 |
+
repr: _ReprArgType = ...,
|
| 75 |
+
hash: bool | None = ...,
|
| 76 |
+
init: bool = ...,
|
| 77 |
+
metadata: Mapping[Any, Any] | None = ...,
|
| 78 |
+
converter: None = ...,
|
| 79 |
+
factory: None = ...,
|
| 80 |
+
kw_only: bool | None = ...,
|
| 81 |
+
eq: bool | None = ...,
|
| 82 |
+
order: bool | None = ...,
|
| 83 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 84 |
+
alias: str | None = ...,
|
| 85 |
+
type: type | None = ...,
|
| 86 |
+
) -> Any: ...
|
| 87 |
+
|
| 88 |
+
# This form catches an explicit None or no default and infers the type from the
|
| 89 |
+
# other arguments.
|
| 90 |
+
@overload
|
| 91 |
+
def field(
|
| 92 |
+
*,
|
| 93 |
+
default: None = ...,
|
| 94 |
+
validator: _ValidatorArgType[_T] | None = ...,
|
| 95 |
+
repr: _ReprArgType = ...,
|
| 96 |
+
hash: bool | None = ...,
|
| 97 |
+
init: bool = ...,
|
| 98 |
+
metadata: Mapping[Any, Any] | None = ...,
|
| 99 |
+
converter: _ConverterType
|
| 100 |
+
| list[_ConverterType]
|
| 101 |
+
| tuple[_ConverterType, ...]
|
| 102 |
+
| None = ...,
|
| 103 |
+
factory: Callable[[], _T] | None = ...,
|
| 104 |
+
kw_only: bool | None = ...,
|
| 105 |
+
eq: _EqOrderType | None = ...,
|
| 106 |
+
order: _EqOrderType | None = ...,
|
| 107 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 108 |
+
alias: str | None = ...,
|
| 109 |
+
type: type | None = ...,
|
| 110 |
+
) -> _T: ...
|
| 111 |
+
|
| 112 |
+
# This form catches an explicit default argument.
|
| 113 |
+
@overload
|
| 114 |
+
def field(
|
| 115 |
+
*,
|
| 116 |
+
default: _T,
|
| 117 |
+
validator: _ValidatorArgType[_T] | None = ...,
|
| 118 |
+
repr: _ReprArgType = ...,
|
| 119 |
+
hash: bool | None = ...,
|
| 120 |
+
init: bool = ...,
|
| 121 |
+
metadata: Mapping[Any, Any] | None = ...,
|
| 122 |
+
converter: _ConverterType
|
| 123 |
+
| list[_ConverterType]
|
| 124 |
+
| tuple[_ConverterType, ...]
|
| 125 |
+
| None = ...,
|
| 126 |
+
factory: Callable[[], _T] | None = ...,
|
| 127 |
+
kw_only: bool | None = ...,
|
| 128 |
+
eq: _EqOrderType | None = ...,
|
| 129 |
+
order: _EqOrderType | None = ...,
|
| 130 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 131 |
+
alias: str | None = ...,
|
| 132 |
+
type: type | None = ...,
|
| 133 |
+
) -> _T: ...
|
| 134 |
+
|
| 135 |
+
# This form covers type=non-Type: e.g. forward references (str), Any
|
| 136 |
+
@overload
|
| 137 |
+
def field(
|
| 138 |
+
*,
|
| 139 |
+
default: _T | None = ...,
|
| 140 |
+
validator: _ValidatorArgType[_T] | None = ...,
|
| 141 |
+
repr: _ReprArgType = ...,
|
| 142 |
+
hash: bool | None = ...,
|
| 143 |
+
init: bool = ...,
|
| 144 |
+
metadata: Mapping[Any, Any] | None = ...,
|
| 145 |
+
converter: _ConverterType
|
| 146 |
+
| list[_ConverterType]
|
| 147 |
+
| tuple[_ConverterType, ...]
|
| 148 |
+
| None = ...,
|
| 149 |
+
factory: Callable[[], _T] | None = ...,
|
| 150 |
+
kw_only: bool | None = ...,
|
| 151 |
+
eq: _EqOrderType | None = ...,
|
| 152 |
+
order: _EqOrderType | None = ...,
|
| 153 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 154 |
+
alias: str | None = ...,
|
| 155 |
+
type: type | None = ...,
|
| 156 |
+
) -> Any: ...
|
| 157 |
+
@overload
|
| 158 |
+
@dataclass_transform(field_specifiers=(attrib, field))
|
| 159 |
+
def define(
|
| 160 |
+
maybe_cls: _C,
|
| 161 |
+
*,
|
| 162 |
+
these: dict[str, Any] | None = ...,
|
| 163 |
+
repr: bool = ...,
|
| 164 |
+
unsafe_hash: bool | None = ...,
|
| 165 |
+
hash: bool | None = ...,
|
| 166 |
+
init: bool = ...,
|
| 167 |
+
slots: bool = ...,
|
| 168 |
+
frozen: bool = ...,
|
| 169 |
+
weakref_slot: bool = ...,
|
| 170 |
+
str: bool = ...,
|
| 171 |
+
auto_attribs: bool = ...,
|
| 172 |
+
kw_only: bool = ...,
|
| 173 |
+
cache_hash: bool = ...,
|
| 174 |
+
auto_exc: bool = ...,
|
| 175 |
+
eq: bool | None = ...,
|
| 176 |
+
order: bool | None = ...,
|
| 177 |
+
auto_detect: bool = ...,
|
| 178 |
+
getstate_setstate: bool | None = ...,
|
| 179 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 180 |
+
field_transformer: _FieldTransformer | None = ...,
|
| 181 |
+
match_args: bool = ...,
|
| 182 |
+
) -> _C: ...
|
| 183 |
+
@overload
|
| 184 |
+
@dataclass_transform(field_specifiers=(attrib, field))
|
| 185 |
+
def define(
|
| 186 |
+
maybe_cls: None = ...,
|
| 187 |
+
*,
|
| 188 |
+
these: dict[str, Any] | None = ...,
|
| 189 |
+
repr: bool = ...,
|
| 190 |
+
unsafe_hash: bool | None = ...,
|
| 191 |
+
hash: bool | None = ...,
|
| 192 |
+
init: bool = ...,
|
| 193 |
+
slots: bool = ...,
|
| 194 |
+
frozen: bool = ...,
|
| 195 |
+
weakref_slot: bool = ...,
|
| 196 |
+
str: bool = ...,
|
| 197 |
+
auto_attribs: bool = ...,
|
| 198 |
+
kw_only: bool = ...,
|
| 199 |
+
cache_hash: bool = ...,
|
| 200 |
+
auto_exc: bool = ...,
|
| 201 |
+
eq: bool | None = ...,
|
| 202 |
+
order: bool | None = ...,
|
| 203 |
+
auto_detect: bool = ...,
|
| 204 |
+
getstate_setstate: bool | None = ...,
|
| 205 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 206 |
+
field_transformer: _FieldTransformer | None = ...,
|
| 207 |
+
match_args: bool = ...,
|
| 208 |
+
) -> Callable[[_C], _C]: ...
|
| 209 |
+
|
| 210 |
+
mutable = define
|
| 211 |
+
|
| 212 |
+
@overload
|
| 213 |
+
@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field))
|
| 214 |
+
def frozen(
|
| 215 |
+
maybe_cls: _C,
|
| 216 |
+
*,
|
| 217 |
+
these: dict[str, Any] | None = ...,
|
| 218 |
+
repr: bool = ...,
|
| 219 |
+
unsafe_hash: bool | None = ...,
|
| 220 |
+
hash: bool | None = ...,
|
| 221 |
+
init: bool = ...,
|
| 222 |
+
slots: bool = ...,
|
| 223 |
+
frozen: bool = ...,
|
| 224 |
+
weakref_slot: bool = ...,
|
| 225 |
+
str: bool = ...,
|
| 226 |
+
auto_attribs: bool = ...,
|
| 227 |
+
kw_only: bool = ...,
|
| 228 |
+
cache_hash: bool = ...,
|
| 229 |
+
auto_exc: bool = ...,
|
| 230 |
+
eq: bool | None = ...,
|
| 231 |
+
order: bool | None = ...,
|
| 232 |
+
auto_detect: bool = ...,
|
| 233 |
+
getstate_setstate: bool | None = ...,
|
| 234 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 235 |
+
field_transformer: _FieldTransformer | None = ...,
|
| 236 |
+
match_args: bool = ...,
|
| 237 |
+
) -> _C: ...
|
| 238 |
+
@overload
|
| 239 |
+
@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field))
|
| 240 |
+
def frozen(
|
| 241 |
+
maybe_cls: None = ...,
|
| 242 |
+
*,
|
| 243 |
+
these: dict[str, Any] | None = ...,
|
| 244 |
+
repr: bool = ...,
|
| 245 |
+
unsafe_hash: bool | None = ...,
|
| 246 |
+
hash: bool | None = ...,
|
| 247 |
+
init: bool = ...,
|
| 248 |
+
slots: bool = ...,
|
| 249 |
+
frozen: bool = ...,
|
| 250 |
+
weakref_slot: bool = ...,
|
| 251 |
+
str: bool = ...,
|
| 252 |
+
auto_attribs: bool = ...,
|
| 253 |
+
kw_only: bool = ...,
|
| 254 |
+
cache_hash: bool = ...,
|
| 255 |
+
auto_exc: bool = ...,
|
| 256 |
+
eq: bool | None = ...,
|
| 257 |
+
order: bool | None = ...,
|
| 258 |
+
auto_detect: bool = ...,
|
| 259 |
+
getstate_setstate: bool | None = ...,
|
| 260 |
+
on_setattr: _OnSetAttrArgType | None = ...,
|
| 261 |
+
field_transformer: _FieldTransformer | None = ...,
|
| 262 |
+
match_args: bool = ...,
|
| 263 |
+
) -> Callable[[_C], _C]: ...
|
| 264 |
+
|
| 265 |
+
class ClassProps:
|
| 266 |
+
# XXX: somehow when defining/using enums Mypy starts looking at our own
|
| 267 |
+
# (untyped) code and causes tons of errors.
|
| 268 |
+
Hashability: Any
|
| 269 |
+
KeywordOnly: Any
|
| 270 |
+
|
| 271 |
+
is_exception: bool
|
| 272 |
+
is_slotted: bool
|
| 273 |
+
has_weakref_slot: bool
|
| 274 |
+
is_frozen: bool
|
| 275 |
+
# kw_only: ClassProps.KeywordOnly
|
| 276 |
+
kw_only: Any
|
| 277 |
+
collected_fields_by_mro: bool
|
| 278 |
+
added_init: bool
|
| 279 |
+
added_repr: bool
|
| 280 |
+
added_eq: bool
|
| 281 |
+
added_ordering: bool
|
| 282 |
+
# hashability: ClassProps.Hashability
|
| 283 |
+
hashability: Any
|
| 284 |
+
added_match_args: bool
|
| 285 |
+
added_str: bool
|
| 286 |
+
added_pickling: bool
|
| 287 |
+
on_setattr_hook: _OnSetAttrType | None
|
| 288 |
+
field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None
|
| 289 |
+
|
| 290 |
+
def __init__(
|
| 291 |
+
self,
|
| 292 |
+
is_exception: bool,
|
| 293 |
+
is_slotted: bool,
|
| 294 |
+
has_weakref_slot: bool,
|
| 295 |
+
is_frozen: bool,
|
| 296 |
+
# kw_only: ClassProps.KeywordOnly
|
| 297 |
+
kw_only: Any,
|
| 298 |
+
collected_fields_by_mro: bool,
|
| 299 |
+
added_init: bool,
|
| 300 |
+
added_repr: bool,
|
| 301 |
+
added_eq: bool,
|
| 302 |
+
added_ordering: bool,
|
| 303 |
+
# hashability: ClassProps.Hashability
|
| 304 |
+
hashability: Any,
|
| 305 |
+
added_match_args: bool,
|
| 306 |
+
added_str: bool,
|
| 307 |
+
added_pickling: bool,
|
| 308 |
+
on_setattr_hook: _OnSetAttrType,
|
| 309 |
+
field_transformer: Callable[[Attribute[Any]], Attribute[Any]],
|
| 310 |
+
) -> None: ...
|
| 311 |
+
@property
|
| 312 |
+
def is_hashable(self) -> bool: ...
|
| 313 |
+
|
| 314 |
+
def inspect(cls: type) -> ClassProps: ...
|
lib/python3.12/site-packages/attrs/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.22 kB). View file
|
|
|
lib/python3.12/site-packages/attrs/__pycache__/converters.cpython-312.pyc
ADDED
|
Binary file (225 Bytes). View file
|
|
|
lib/python3.12/site-packages/attrs/__pycache__/exceptions.cpython-312.pyc
ADDED
|
Binary file (225 Bytes). View file
|
|
|
lib/python3.12/site-packages/attrs/__pycache__/filters.cpython-312.pyc
ADDED
|
Binary file (219 Bytes). View file
|
|
|
lib/python3.12/site-packages/attrs/__pycache__/setters.cpython-312.pyc
ADDED
|
Binary file (219 Bytes). View file
|
|
|
lib/python3.12/site-packages/attrs/__pycache__/validators.cpython-312.pyc
ADDED
|
Binary file (225 Bytes). View file
|
|
|
lib/python3.12/site-packages/attrs/converters.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: MIT
|
| 2 |
+
|
| 3 |
+
from attr.converters import * # noqa: F403
|
lib/python3.12/site-packages/attrs/exceptions.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: MIT
|
| 2 |
+
|
| 3 |
+
from attr.exceptions import * # noqa: F403
|
lib/python3.12/site-packages/attrs/filters.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: MIT
|
| 2 |
+
|
| 3 |
+
from attr.filters import * # noqa: F403
|
lib/python3.12/site-packages/attrs/py.typed
ADDED
|
File without changes
|
lib/python3.12/site-packages/attrs/setters.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: MIT
|
| 2 |
+
|
| 3 |
+
from attr.setters import * # noqa: F403
|
lib/python3.12/site-packages/attrs/validators.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-License-Identifier: MIT
|
| 2 |
+
|
| 3 |
+
from attr.validators import * # noqa: F403
|
lib/python3.12/site-packages/torchaudio/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Initialize extension and backend first
|
| 2 |
+
from . import _extension # noqa # usort: skip
|
| 3 |
+
from ._backend import ( # noqa # usort: skip
|
| 4 |
+
AudioMetaData,
|
| 5 |
+
get_audio_backend,
|
| 6 |
+
info,
|
| 7 |
+
list_audio_backends,
|
| 8 |
+
load,
|
| 9 |
+
save,
|
| 10 |
+
set_audio_backend,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
from . import ( # noqa: F401
|
| 14 |
+
compliance,
|
| 15 |
+
datasets,
|
| 16 |
+
functional,
|
| 17 |
+
io,
|
| 18 |
+
kaldi_io,
|
| 19 |
+
models,
|
| 20 |
+
pipelines,
|
| 21 |
+
sox_effects,
|
| 22 |
+
transforms,
|
| 23 |
+
utils,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
# For BC
|
| 27 |
+
from . import backend # noqa # usort: skip
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
from .version import __version__, git_version # noqa: F401
|
| 31 |
+
except ImportError:
|
| 32 |
+
pass
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
__all__ = [
|
| 36 |
+
"AudioMetaData",
|
| 37 |
+
"load",
|
| 38 |
+
"info",
|
| 39 |
+
"save",
|
| 40 |
+
"io",
|
| 41 |
+
"compliance",
|
| 42 |
+
"datasets",
|
| 43 |
+
"functional",
|
| 44 |
+
"models",
|
| 45 |
+
"pipelines",
|
| 46 |
+
"kaldi_io",
|
| 47 |
+
"utils",
|
| 48 |
+
"sox_effects",
|
| 49 |
+
"transforms",
|
| 50 |
+
"list_audio_backends",
|
| 51 |
+
"get_audio_backend",
|
| 52 |
+
"set_audio_backend",
|
| 53 |
+
]
|
lib/python3.12/site-packages/torchaudio/_internal/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from .fb import download_url_to_file, load_state_dict_from_url
|
| 3 |
+
except ImportError:
|
| 4 |
+
from torch.hub import download_url_to_file, load_state_dict_from_url
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"load_state_dict_from_url",
|
| 9 |
+
"download_url_to_file",
|
| 10 |
+
]
|
lib/python3.12/site-packages/torchaudio/_internal/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (438 Bytes). View file
|
|
|
lib/python3.12/site-packages/torchaudio/_internal/__pycache__/module_utils.cpython-312.pyc
ADDED
|
Binary file (5.66 kB). View file
|
|
|
lib/python3.12/site-packages/torchaudio/_internal/module_utils.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib.util
|
| 2 |
+
import os
|
| 3 |
+
import warnings
|
| 4 |
+
from functools import wraps
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def eval_env(var, default):
|
| 9 |
+
"""Check if environment varable has True-y value"""
|
| 10 |
+
if var not in os.environ:
|
| 11 |
+
return default
|
| 12 |
+
|
| 13 |
+
val = os.environ.get(var, "0")
|
| 14 |
+
trues = ["1", "true", "TRUE", "on", "ON", "yes", "YES"]
|
| 15 |
+
falses = ["0", "false", "FALSE", "off", "OFF", "no", "NO"]
|
| 16 |
+
if val in trues:
|
| 17 |
+
return True
|
| 18 |
+
if val not in falses:
|
| 19 |
+
# fmt: off
|
| 20 |
+
raise RuntimeError(
|
| 21 |
+
f"Unexpected environment variable value `{var}={val}`. "
|
| 22 |
+
f"Expected one of {trues + falses}")
|
| 23 |
+
# fmt: on
|
| 24 |
+
return False
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def is_module_available(*modules: str) -> bool:
|
| 28 |
+
r"""Returns if a top-level module with :attr:`name` exists *without**
|
| 29 |
+
importing it. This is generally safer than try-catch block around a
|
| 30 |
+
`import X`. It avoids third party libraries breaking assumptions of some of
|
| 31 |
+
our tests, e.g., setting multiprocessing start method when imported
|
| 32 |
+
(see librosa/#747, torchvision/#544).
|
| 33 |
+
"""
|
| 34 |
+
return all(importlib.util.find_spec(m) is not None for m in modules)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def requires_module(*modules: str):
|
| 38 |
+
"""Decorate function to give error message if invoked without required optional modules.
|
| 39 |
+
|
| 40 |
+
This decorator is to give better error message to users rather
|
| 41 |
+
than raising ``NameError: name 'module' is not defined`` at random places.
|
| 42 |
+
"""
|
| 43 |
+
missing = [m for m in modules if not is_module_available(m)]
|
| 44 |
+
|
| 45 |
+
if not missing:
|
| 46 |
+
# fall through. If all the modules are available, no need to decorate
|
| 47 |
+
def decorator(func):
|
| 48 |
+
return func
|
| 49 |
+
|
| 50 |
+
else:
|
| 51 |
+
req = f"module: {missing[0]}" if len(missing) == 1 else f"modules: {missing}"
|
| 52 |
+
|
| 53 |
+
def decorator(func):
|
| 54 |
+
@wraps(func)
|
| 55 |
+
def wrapped(*args, **kwargs):
|
| 56 |
+
raise RuntimeError(f"{func.__module__}.{func.__name__} requires {req}")
|
| 57 |
+
|
| 58 |
+
return wrapped
|
| 59 |
+
|
| 60 |
+
return decorator
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def deprecated(direction: str, version: Optional[str] = None, remove: bool = False):
|
| 64 |
+
"""Decorator to add deprecation message
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
direction (str): Migration steps to be given to users.
|
| 68 |
+
version (str or int): The version when the object will be removed
|
| 69 |
+
remove (bool): If enabled, append future removal message.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def decorator(func):
|
| 73 |
+
@wraps(func)
|
| 74 |
+
def wrapped(*args, **kwargs):
|
| 75 |
+
message = f"{func.__module__}.{func.__name__} has been deprecated. {direction}"
|
| 76 |
+
if remove:
|
| 77 |
+
message += f' It will be removed from {"future" if version is None else version} release. '
|
| 78 |
+
warnings.warn(message, stacklevel=2)
|
| 79 |
+
return func(*args, **kwargs)
|
| 80 |
+
|
| 81 |
+
message = "This function has been deprecated. "
|
| 82 |
+
if remove:
|
| 83 |
+
message += f'It will be removed from {"future" if version is None else version} release. '
|
| 84 |
+
|
| 85 |
+
wrapped.__doc__ = f"""DEPRECATED: {func.__doc__}
|
| 86 |
+
|
| 87 |
+
.. warning::
|
| 88 |
+
|
| 89 |
+
{message}
|
| 90 |
+
{direction}
|
| 91 |
+
"""
|
| 92 |
+
|
| 93 |
+
return wrapped
|
| 94 |
+
|
| 95 |
+
return decorator
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def fail_with_message(message):
|
| 99 |
+
"""Generate decorator to give users message about missing TorchAudio extension."""
|
| 100 |
+
|
| 101 |
+
def decorator(func):
|
| 102 |
+
@wraps(func)
|
| 103 |
+
def wrapped(*args, **kwargs):
|
| 104 |
+
raise RuntimeError(f"{func.__module__}.{func.__name__} {message}")
|
| 105 |
+
|
| 106 |
+
return wrapped
|
| 107 |
+
|
| 108 |
+
return decorator
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def no_op(func):
|
| 112 |
+
"""Op-op decorator. Used in place of fail_with_message when a functionality that requires extension works fine."""
|
| 113 |
+
return func
|
lib/python3.12/site-packages/torchaudio/compliance/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from . import kaldi
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
"kaldi",
|
| 5 |
+
]
|
lib/python3.12/site-packages/torchaudio/compliance/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (265 Bytes). View file
|
|
|