diff --git a/.gitignore b/.gitignore index 22b5395..77783cc 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,7 @@ MANIFEST .vscode/* examples/*.py anaconda_credentials.txt + +node_modules +yarn.lock +src/pyforest/static/extension* diff --git a/README.md b/README.md index 0cf97bb..ac9712b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,14 @@ When you are done with your script, you can export all import statements via: active_imports() ``` +You can also install the notebook/lab extensions to automatically add the imports to the first cell, as they're imported: + +``` +python -m pyforest install_extensions +``` + +![demo](examples/assets/pyforest_demo_extensions.gif) + Which libraries are available? - We aim to add all popular Python Data Science libraries which should account for >99% of your daily imports. For example, `pandas` as `pd`, `numpy` as `np`, `seaborn` as `sns`, `matplotlib.pyplot` as `plt`, or `OneHotEncoder` from `sklearn` and many more. In addition, there are also helper modules like `os`, `re`, `tqdm`, or `Path` from `pathlib`. - You can see an overview of all available lazy imports if you type `lazy_imports()` in Python. diff --git a/examples/assets/pyforest_demo_extensions.gif b/examples/assets/pyforest_demo_extensions.gif new file mode 100644 index 0000000..adeea47 Binary files /dev/null and b/examples/assets/pyforest_demo_extensions.gif differ diff --git a/setup.py b/setup.py index 8972789..47b9bb7 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,58 @@ -# -*- coding: utf-8 -*- -""" - Setup file for pyforest. - Use setup.cfg to configure your project. -""" -import sys +from setuptools import setup, find_packages +from setuptools.command.sdist import sdist +from setuptools.command.build_py import build_py +from setuptools.command.egg_info import egg_info +from setuptools.command.install import install +from subprocess import check_call +import platform -from pkg_resources import VersionConflict, require -from setuptools import setup -from src.pyforest.auto_import import setup as setup_auto_import +from src.pyforest import auto_import +from src.pyforest._version import __version__ -try: - require("setuptools>=38.3") -except VersionConflict: - print("Error: version of setuptools is too old (<38.3)!") - sys.exit(1) + +def wrap_command(command, setup_auto_import=False): + class DecoratedCommand(command): + def run(self): + suffix = ('.cmd' if platform.system() == 'Windows' else '') + check_call(['npm' + suffix, 'install'], cwd='src/pyforest') + check_call(['webpack' + suffix], cwd='src/pyforest') + command.run(self) + if setup_auto_import: + auto_import.setup() + return DecoratedCommand + + +setup_args = { + 'name': 'pyforest', + 'version': __version__, + 'include_package_data': True, + 'package_data': { + 'pyforest': ['static/*.js', 'package.json', 'package-lock.json'], + }, + 'packages': find_packages(), + 'zip_safe': False, + 'cmdclass': { + 'install': wrap_command(install, setup_auto_import=True), + 'build_py': wrap_command(build_py), + 'egg_info': wrap_command(egg_info), + 'sdist': wrap_command(sdist), + }, + 'author': '8080labs', + 'url': 'https://github.com/8080labs/pyforest', + 'keywords': [ + 'ipython', + 'jupyter', + 'jupyterlab', + ], + 'classifiers': [ + 'Development Status :: 4 - Beta', + 'Framework :: IPython', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'Programming Language :: Python :: 3', + ], +} if __name__ == "__main__": - setup() - setup_auto_import() + setup(**setup_args) diff --git a/src/pyforest/__init__.py b/src/pyforest/__init__.py index 837699f..dbc2388 100644 --- a/src/pyforest/__init__.py +++ b/src/pyforest/__init__.py @@ -11,3 +11,19 @@ __version__ = "unknown" finally: del get_distribution, DistributionNotFound + + +def _jupyter_nbextension_paths(): + return [{ + 'section': 'notebook', + 'src': 'static', + 'dest': 'pyforest', + 'require': 'pyforest/extension' + }] + + +def _jupyter_labextension_paths(): + return [{ + 'name': 'pyforest', + 'src': 'static', + }] diff --git a/src/pyforest/__main__.py b/src/pyforest/__main__.py new file mode 100644 index 0000000..d5db20f --- /dev/null +++ b/src/pyforest/__main__.py @@ -0,0 +1,42 @@ +def main(): + import sys + + if len(sys.argv) != 2 or sys.argv[1] != "install_extensions": + print(USAGE) + sys.exit(-1) + + install_nbextension() + install_labextension() + +def install_nbextension(): + try: + from notebook import nbextensions + except ImportError: + # No notebook + return + + print("Installing nbextension...") + nbextensions.install_nbextension_python('pyforest') + nbextensions.enable_nbextension_python('pyforest') + +def install_labextension(): + try: + from jupyterlab import commands + except ImportError: + # No jupyterlab + return + + from pathlib import Path + dir = Path(__file__).parent + + print("Installing labextension...") + should_build = commands.install_extension(str(dir)) + if should_build: + commands.build() + +USAGE = """Usage: python -m pyforest install_extensions + installs notebook/lab extensions +""" + +if __name__ == "__main__": + main() diff --git a/src/pyforest/_importable.py b/src/pyforest/_importable.py index 1b792a0..f0efa43 100644 --- a/src/pyforest/_importable.py +++ b/src/pyforest/_importable.py @@ -35,8 +35,11 @@ def __maybe_import_complementary_imports__(self): def __maybe_import__(self): self.__maybe_import_complementary_imports__() exec(self.__import_statement__, globals()) + prev_imported = self.__was_imported__ # Attention: if the import fails, the next line will not be reached self.__was_imported__ = True + if not prev_imported: + _update_import_cell() # among others, called during auto-completion of IPython/Jupyter def __dir__(self): @@ -69,6 +72,25 @@ def __repr__(self, *args, **kwargs): return f"lazy pyforest.LazyImport for '{self.__import_statement__}'" +def _update_import_cell(): + try: + from IPython.display import display, Javascript + except ImportError: + return + + import inspect + from .widget import PyforestWidget + + statements = [] + # TODO: Figure out why it's not at a fixed level/a less fragile way + for frame in inspect.stack()[2:]: + lazy_imports = {s for s in frame[0].f_globals.values() if isinstance(s, LazyImport)} + if lazy_imports: + statements = [s.__import_statement__ for s in lazy_imports if s.__was_imported__] + break + display(PyforestWidget(imports=statements)) + + def _import_statements(symbol_dict, was_imported=True): statements = [] for _, symbol in symbol_dict.items(): diff --git a/src/pyforest/_version.py b/src/pyforest/_version.py new file mode 100644 index 0000000..615fc95 --- /dev/null +++ b/src/pyforest/_version.py @@ -0,0 +1,8 @@ +version_info = (0, 1, 2, 'final', 0) + +_specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''} + +__version__ = '{}.{}.{}{}'.format( + version_info[0], version_info[1], version_info[2], + '' if version_info[3]=='final' else _specifier_[version_info[3]]+str(version_info[4]) +) diff --git a/src/pyforest/package-lock.json b/src/pyforest/package-lock.json new file mode 100644 index 0000000..0318397 --- /dev/null +++ b/src/pyforest/package-lock.json @@ -0,0 +1,336 @@ +{ + "name": "pyforest", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@jupyter-widgets/base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@jupyter-widgets/base/-/base-2.0.1.tgz", + "integrity": "sha512-jRNeQzvEf6MMGUaEPfBzgZ+IT4XOInpw8Ef+CunNUmUCiQZ8WfNpFR671S0Vu3xO6yGKMTU2QPdk8/aCsJRw9w==", + "requires": { + "@jupyterlab/services": "4.1.0", + "@phosphor/coreutils": "1.3.1", + "@phosphor/messaging": "1.3.0", + "@phosphor/widgets": "1.9.0", + "@types/backbone": "1.4.1", + "@types/lodash": "4.14.138", + "backbone": "1.2.3", + "base64-js": "1.3.1", + "jquery": "3.4.1", + "lodash": "4.17.15" + } + }, + "@jupyterlab/coreutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-3.1.0.tgz", + "integrity": "sha512-ZqgzDUyanyvc86gtCrIbc1M6iniKHYmWNWHvWOcnq3KIP3wk3grchsTYPTfQDxcUS6F04baPGp/KohEU2ml40Q==", + "requires": { + "@phosphor/commands": "1.7.0", + "@phosphor/coreutils": "1.3.1", + "@phosphor/disposable": "1.3.0", + "@phosphor/properties": "1.1.3", + "@phosphor/signaling": "1.3.0", + "ajv": "6.10.2", + "json5": "2.1.0", + "minimist": "1.2.0", + "moment": "2.24.0", + "path-posix": "1.0.0", + "url-parse": "1.4.7" + } + }, + "@jupyterlab/observables": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.3.0.tgz", + "integrity": "sha512-I3Cmu0qUU3Emzai0MRBmiSHd1c42DtOuDYAru0/cvlI8xQKzin7t/EF9TahxSEBEDDNU4RRjADUYG9T3WefYtA==", + "requires": { + "@phosphor/algorithm": "1.2.0", + "@phosphor/coreutils": "1.3.1", + "@phosphor/disposable": "1.3.0", + "@phosphor/messaging": "1.3.0", + "@phosphor/signaling": "1.3.0" + } + }, + "@jupyterlab/services": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-4.1.0.tgz", + "integrity": "sha512-5C53UdpLP5rLLpN8zEfzole5PNakNWSDCitd4ysqZiWihV4lpNNXKmqn5seHrcvevUkrDM9pyNcg00djGkpbvw==", + "requires": { + "@jupyterlab/coreutils": "3.1.0", + "@jupyterlab/observables": "2.3.0", + "@phosphor/algorithm": "1.2.0", + "@phosphor/coreutils": "1.3.1", + "@phosphor/disposable": "1.3.0", + "@phosphor/signaling": "1.3.0", + "node-fetch": "2.6.0", + "ws": "7.1.2" + } + }, + "@phosphor/algorithm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.2.0.tgz", + "integrity": "sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==" + }, + "@phosphor/collections": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/collections/-/collections-1.2.0.tgz", + "integrity": "sha512-T9/0EjSuY6+ga2LIFRZ0xupciOR3Qnyy8Q95lhGTC0FXZUFwC8fl9e8On6IcwasCszS+1n8dtZUWSIynfgdpzw==", + "requires": { + "@phosphor/algorithm": "1.2.0" + } + }, + "@phosphor/commands": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@phosphor/commands/-/commands-1.7.0.tgz", + "integrity": "sha512-2tkij4fiU0WcnUiY0H0kX9mQhdJms5X6s+X9Uzx5P+KJZm0B83VIC1OEb1YYDYh8ar+LMr0dBnM5ljvZbptUXw==", + "requires": { + "@phosphor/algorithm": "1.2.0", + "@phosphor/coreutils": "1.3.1", + "@phosphor/disposable": "1.3.0", + "@phosphor/domutils": "1.1.3", + "@phosphor/keyboard": "1.1.3", + "@phosphor/signaling": "1.3.0" + } + }, + "@phosphor/coreutils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.1.tgz", + "integrity": "sha512-9OHCn8LYRcPU/sbHm5v7viCA16Uev3gbdkwqoQqlV+EiauDHl70jmeL7XVDXdigl66Dz0LI11C99XOxp+s3zOA==" + }, + "@phosphor/disposable": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.3.0.tgz", + "integrity": "sha512-wHQov7HoS20mU6yuEz5ZMPhfxHdcxGovjPoid0QwccUEOm33UBkWlxaJGm9ONycezIX8je7ZuPOf/gf7JI6Dlg==", + "requires": { + "@phosphor/algorithm": "1.2.0", + "@phosphor/signaling": "1.3.0" + } + }, + "@phosphor/domutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@phosphor/domutils/-/domutils-1.1.3.tgz", + "integrity": "sha512-5CtLAhURQXXHhNXfQydDk/luG1cDVnhlu/qw7gz8/9pht0KXIAmNg/M0LKxx2oJ9+YMNCLVWxAnHAU0yrDpWSA==" + }, + "@phosphor/dragdrop": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@phosphor/dragdrop/-/dragdrop-1.4.0.tgz", + "integrity": "sha512-JqmDAKczviUe7NEkiDf/A6H2glgVmHAREip8dGBli4lvV+CQqPFyl4Xm7XCnR9qiEqNrP+0SfwPpywNa0me3nQ==", + "requires": { + "@phosphor/coreutils": "1.3.1", + "@phosphor/disposable": "1.3.0" + } + }, + "@phosphor/keyboard": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@phosphor/keyboard/-/keyboard-1.1.3.tgz", + "integrity": "sha512-dzxC/PyHiD6mXaESRy6PZTd9JeK+diwG1pyngkyUf127IXOEzubTIbu52VSdpGBklszu33ws05BAGDa4oBE4mQ==" + }, + "@phosphor/messaging": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.3.0.tgz", + "integrity": "sha512-k0JE+BTMKlkM335S2AmmJxoYYNRwOdW5jKBqLgjJdGRvUQkM0+2i60ahM45+J23atGJDv9esKUUBINiKHFhLew==", + "requires": { + "@phosphor/algorithm": "1.2.0", + "@phosphor/collections": "1.2.0" + } + }, + "@phosphor/properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@phosphor/properties/-/properties-1.1.3.tgz", + "integrity": "sha512-GiglqzU77s6+tFVt6zPq9uuyu/PLQPFcqZt914ZhJ4cN/7yNI/SLyMzpYZ56IRMXvzK9TUgbRna6URE3XAwFUg==" + }, + "@phosphor/signaling": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.3.0.tgz", + "integrity": "sha512-ZbG2Mof4LGSkaEuDicqA2o2TKu3i5zanjr2GkevI/82aKBD7cI1NGLGT55HZwtE87/gOF4FIM3d3DeyrFDMjMQ==", + "requires": { + "@phosphor/algorithm": "1.2.0" + } + }, + "@phosphor/virtualdom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@phosphor/virtualdom/-/virtualdom-1.2.0.tgz", + "integrity": "sha512-L9mKNhK2XtVjzjuHLG2uYuepSz8uPyu6vhF4EgCP0rt0TiLYaZeHwuNu3XeFbul9DMOn49eBpye/tfQVd4Ks+w==", + "requires": { + "@phosphor/algorithm": "1.2.0" + } + }, + "@phosphor/widgets": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@phosphor/widgets/-/widgets-1.9.0.tgz", + "integrity": "sha512-Op6H0lI7MlHAs+htzy+6fL+x3TxG0N024RGsdIYkaNKyeSw6b7ZUXcd7mKCeabWPoTwiMCx3kiLTvExmNSYgwA==", + "requires": { + "@phosphor/algorithm": "1.2.0", + "@phosphor/commands": "1.7.0", + "@phosphor/coreutils": "1.3.1", + "@phosphor/disposable": "1.3.0", + "@phosphor/domutils": "1.1.3", + "@phosphor/dragdrop": "1.4.0", + "@phosphor/keyboard": "1.1.3", + "@phosphor/messaging": "1.3.0", + "@phosphor/properties": "1.1.3", + "@phosphor/signaling": "1.3.0", + "@phosphor/virtualdom": "1.2.0" + } + }, + "@types/backbone": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@types/backbone/-/backbone-1.4.1.tgz", + "integrity": "sha512-KYfGuQy4d2vvYXbn0uHFZ6brFLndatTMomxBlljpbWf4kFpA3BG/6LA3ec+J9iredrX6eAVI7sm9SVAvwiIM6g==", + "requires": { + "@types/jquery": "3.3.31", + "@types/underscore": "1.9.2" + } + }, + "@types/jquery": { + "version": "3.3.31", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz", + "integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==", + "requires": { + "@types/sizzle": "2.3.2" + } + }, + "@types/lodash": { + "version": "4.14.138", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.138.tgz", + "integrity": "sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg==" + }, + "@types/sizzle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", + "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==" + }, + "@types/underscore": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.9.2.tgz", + "integrity": "sha512-KgOKTAD+9X+qvZnB5S1+onqKc4E+PZ+T6CM/NA5ohRPLHJXb+yCJMVf8pWOnvuBuKFNUAJW8N97IA6lba6mZGg==" + }, + "ajv": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "requires": { + "fast-deep-equal": "2.0.1", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.4.1", + "uri-js": "4.2.2" + } + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "backbone": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.2.3.tgz", + "integrity": "sha1-wiz9B/yG676uYdGJKe0RXpmdZbk=", + "requires": { + "underscore": "1.9.1" + } + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "jquery": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json5": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", + "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "requires": { + "minimist": "1.2.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, + "path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "querystringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", + "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==" + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "2.1.1" + } + }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "2.1.1", + "requires-port": "1.0.0" + } + }, + "ws": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz", + "integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==", + "requires": { + "async-limiter": "1.0.1" + } + } + } +} diff --git a/src/pyforest/package.json b/src/pyforest/package.json new file mode 100644 index 0000000..3856473 --- /dev/null +++ b/src/pyforest/package.json @@ -0,0 +1,24 @@ +{ + "private": true, + "name": "pyforest", + "version": "0.0.1", + "description": "Automatically adds lazy imports to first cell.", + "author": "valtron", + "main": "static/labextension.js", + "keywords": [ + "jupyter", + "jupyterlab", + "jupyterlab-extension" + ], + "files": [ + "static/*.js" + ], + "jupyterlab": { + "extension": true + }, + "scripts": {}, + "dependencies": { + "@jupyter-widgets/base": "^1.1 || ^2", + "lodash": "^4.17.4" + } +} diff --git a/src/pyforest/static/common.js b/src/pyforest/static/common.js new file mode 100644 index 0000000..382b6bf --- /dev/null +++ b/src/pyforest/static/common.js @@ -0,0 +1,49 @@ +define(['lodash', '@jupyter-widgets/base', '../package.json'], function (_, widgets, package_) { + function makeWidget(getFirstCellContent, setFirstCellContent) { + const PyforestWidget = widgets.DOMWidgetView.extend({ + render: function () { + const content = getFirstCellContent(); + setFirstCellContent(updateImports(this.model.get('imports'), content)); + } + }); + + const PyforestWidgetModel = widgets.DOMWidgetModel.extend({ + defaults: _.extend(widgets.DOMWidgetModel.prototype.defaults(), { + _model_name: 'PyforestWidgetModel', + _view_name: 'PyforestWidget', + _model_module: package_.name, + _view_module: package_.name, + _model_module_version: package_.version, + _view_module_version: package_.version, + imports: [] + }) + }); + + return { + PyforestWidget: PyforestWidget, + PyforestWidgetModel: PyforestWidgetModel + }; + } + + const begin = '# begin pyforest imports'; + const end = '# end pyforest imports'; + const any = '[\\s\\S]*'; + const importRegex = new RegExp([ + '^(', any, ')', begin, '\n', any, end, '(', any, ')$' + ].join(''), 'mi'); + + function updateImports(imports, currentContent) { + let match = currentContent.match(importRegex); + if (match == null) { + match = ['', currentContent, '']; + } + if (match[1].length > 0 && match[1].substr(-1) != '\n') { + match[1] += '\n' + } + return [match[1], begin + '\n', imports.join('\n') + '\n', end, match[2]].join(''); + } + + return { + makeWidget: makeWidget + }; +}); diff --git a/src/pyforest/static/labextension.js b/src/pyforest/static/labextension.js new file mode 100644 index 0000000..eb1f24f --- /dev/null +++ b/src/pyforest/static/labextension.js @@ -0,0 +1,26 @@ +const notebook = require('@jupyterlab/notebook'); +const widgets = require('@jupyter-widgets/base'); +const package_ = require('../package.json'); +const common = require('./common.js'); + +module.exports = [{ + id: 'pyforest', + autoStart: true, + requires: [notebook.INotebookTracker, widgets.IJupyterWidgetRegistry], + activate: function (app, notebookTracker, widgetRegistry) { + const widgetExports = common.makeWidget( + function () { return getFirstCellDoc().text; }, + function (content) { getFirstCellDoc().text = content; } + ); + + function getFirstCellDoc() { + return notebookTracker.currentWidget.model.cells.get(0).value; + } + + widgetRegistry.registerWidget({ + name: 'pyforest', + version: package_.version, + exports: widgetExports + }); + } +}]; diff --git a/src/pyforest/static/nbextension.js b/src/pyforest/static/nbextension.js new file mode 100644 index 0000000..02ddb07 --- /dev/null +++ b/src/pyforest/static/nbextension.js @@ -0,0 +1,14 @@ +if (window.require) { + window.require.config({ + map: { + "*" : { + "pyforest": "nbextensions/pyforest/extension_impl", + } + } + }); +} + +// Export the required load_ipython_extension +module.exports = { + load_ipython_extension: function() {} +}; diff --git a/src/pyforest/static/nbextension_impl.js b/src/pyforest/static/nbextension_impl.js new file mode 100644 index 0000000..194e512 --- /dev/null +++ b/src/pyforest/static/nbextension_impl.js @@ -0,0 +1,11 @@ +const Jupyter = require('base/js/namespace'); +const common = require('./common.js'); + +function getFirstCellDoc() { + return Jupyter.notebook.get_cell(0).code_mirror.getDoc(); +} + +module.exports = common.makeWidget( + function () { return getFirstCellDoc().getValue(); }, + function (content) { getFirstCellDoc().setValue(content); } +); diff --git a/src/pyforest/webpack.config.js b/src/pyforest/webpack.config.js new file mode 100644 index 0000000..f95e97e --- /dev/null +++ b/src/pyforest/webpack.config.js @@ -0,0 +1,23 @@ +var path = require('path'); + +module.exports = [ + { + entry: './static/nbextension.js', + output: { + filename: 'extension.js', + path: path.resolve(__dirname, '..', 'pyforest', 'static'), + libraryTarget: 'amd' + }, + externals: [], + }, + { + entry: './static/nbextension_impl.js', + output: { + filename: 'extension_impl.js', + path: path.resolve(__dirname, '..', 'pyforest', 'static'), + libraryTarget: 'amd' + }, + devtool: 'source-map', + externals: ['base/js/namespace'] + }, +]; diff --git a/src/pyforest/widget.py b/src/pyforest/widget.py new file mode 100644 index 0000000..5de3599 --- /dev/null +++ b/src/pyforest/widget.py @@ -0,0 +1,21 @@ +from pathlib import Path +import json +import ipywidgets +from traitlets import Unicode, List + + +with (Path(__file__).parent / 'package.json').open('rt') as fh: + npm_package = json.load(fh) + + +@ipywidgets.register +class PyforestWidget(ipywidgets.DOMWidget): + _view_module = Unicode(npm_package['name']).tag(sync=True) + _view_name = Unicode('PyforestWidget').tag(sync=True) + _view_module_version = Unicode('^' + npm_package['version']).tag(sync=True) + + _model_module = Unicode(npm_package['name']).tag(sync=True) + _model_name = Unicode('PyforestWidgetModel').tag(sync=True) + _model_module_version = Unicode('^' + npm_package['version']).tag(sync=True) + + imports = List(Unicode()).tag(sync=True)