From 3cd1de8aa632efd9b1e03651aa01aa1ad6fbeaa7 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Mon, 14 Mar 2022 03:38:00 -0700 Subject: [PATCH 01/78] Update 6.4.x branch with some missing commits (#6308) Co-authored-by: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Vishwajeet <33215443+Vishwajeet0510@users.noreply.github.com> Co-authored-by: Kevin Bates --- CHANGELOG.md | 18 ++++++++++++++++-- notebook/services/kernels/handlers.py | 25 ++++++++++++++++++++++--- notebook/static/base/js/namespace.js | 2 +- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a072eecb..e0fef4f52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,22 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with +## 6.4.8 + +([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...479902d83a691253e0cff8439a33577e82408317)) + +### Bugs fixed + +- Fix to remove potential memory leak on Jupyter Notebooks ZMQChannelHandler code [#6251](https://github.com/jupyter/notebook/pull/6251) ([@Vishwajeet0510](https://github.com/Vishwajeet0510)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/jupyter/notebook/graphs/contributors?from=2022-01-12&to=2022-01-25&type=c)) + +[@Vishwajeet0510](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3AVishwajeet0510+updated%3A2022-01-12..2022-01-25&type=Issues) + + + ## 6.4.7 ([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.6...b77b5e38b8fa1a20150d7fa4d735dbf1c4f00418)) @@ -40,8 +56,6 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with [@antoinecarme](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Aantoinecarme+updated%3A2021-11-16..2022-01-12&type=Issues) | [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ablink1073+updated%3A2021-11-16..2022-01-12&type=Issues) | [@ccw630](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Accw630+updated%3A2021-11-16..2022-01-12&type=Issues) | [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Akevin-bates+updated%3A2021-11-16..2022-01-12&type=Issues) | [@LiHua-Official](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3ALiHua-Official+updated%3A2021-11-16..2022-01-12&type=Issues) | [@penguinolog](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Apenguinolog+updated%3A2021-11-16..2022-01-12&type=Issues) | [@tornaria](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Atornaria+updated%3A2021-11-16..2022-01-12&type=Issues) - - ## 6.4.6 ([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.5...160c27d3c23dafe8b42240571db21b0d5cbae2fe)) diff --git a/notebook/services/kernels/handlers.py b/notebook/services/kernels/handlers.py index 2b7f89b6f8..a2d4ddbab9 100644 --- a/notebook/services/kernels/handlers.py +++ b/notebook/services/kernels/handlers.py @@ -138,7 +138,7 @@ def nudge(self): """Nudge the zmq connections with kernel_info_requests Returns a Future that will resolve when we have received - a shell reply and at least one iopub message, + a shell or control reply and at least one iopub message, ensuring that zmq subscriptions are established, sockets are fully connected, and kernel is responsive. @@ -157,10 +157,12 @@ def nudge(self): f = Future() f.set_result(None) return f - # Use a transient shell channel to prevent leaking # shell responses to the front-end. shell_channel = kernel.connect_shell() + # Use a transient control channel to prevent leaking + # control responses to the front-end. + control_channel = kernel.connect_control() # The IOPub used by the client, whose subscriptions we are verifying. iopub_channel = self.channels["iopub"] @@ -183,6 +185,8 @@ def cleanup(f=None): iopub_channel.stop_on_recv() if not shell_channel.closed(): shell_channel.close() + if not control_channel.closed(): + control_channel.close() # trigger cleanup when both message futures are resolved both_done.add_done_callback(cleanup) @@ -193,6 +197,12 @@ def on_shell_reply(msg): self.log.debug("Nudge: resolving shell future: %s", self.kernel_id) info_future.set_result(None) + def on_control_reply(msg): + self.log.debug("Nudge: control info reply received: %s", self.kernel_id) + if not info_future.done(): + self.log.debug("Nudge: resolving control future: %s", self.kernel_id) + info_future.set_result(None) + def on_iopub(msg): self.log.debug("Nudge: IOPub received: %s", self.kernel_id) if not iopub_future.done(): @@ -202,6 +212,7 @@ def on_iopub(msg): iopub_channel.on_recv(on_iopub) shell_channel.on_recv(on_shell_reply) + control_channel.on_recv(on_control_reply) loop = IOLoop.current() # Nudge the kernel with kernel info requests until we get an IOPub message @@ -227,6 +238,12 @@ def nudge(count): # check for closed zmq socket if shell_channel.closed(): + self.log.debug("Nudge: cancelling on closed zmq socket: %s", self.kernel_id) + finish() + return + + # check for closed zmq socket + if control_channel.closed(): self.log.debug( "Nudge: cancelling on closed zmq socket: %s", self.kernel_id ) @@ -237,6 +254,7 @@ def nudge(count): log = self.log.warning if count % 10 == 0 else self.log.debug log("Nudge: attempt %s on kernel %s" % (count, self.kernel_id)) self.session.send(shell_channel, "kernel_info_request") + self.session.send(control_channel, "kernel_info_request") nonlocal nudge_handle nudge_handle = loop.call_later(0.5, nudge, count) @@ -371,7 +389,8 @@ def _register_session(self): if stale_handler: self.log.warning("Replacing stale connection: %s", self.session_key) yield stale_handler.close() - self._open_sessions[self.session_key] = self + if self.kernel_id in self.kernel_manager: # only update open sessions if kernel is actively managed + self._open_sessions[self.session_key] = self def open(self, kernel_id): super().open() diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 336e07acf2..2705bedebf 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.4.7"; + Jupyter.version = "6.4.8"; Jupyter._target = '_blank'; return Jupyter; From 6f73a5722615be297f8e434a24c41041437da622 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Mon, 14 Mar 2022 05:47:19 -0500 Subject: [PATCH 02/78] Specify minimum version of nbconvert required (#6286) Co-authored-by: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Vishwajeet <33215443+Vishwajeet0510@users.noreply.github.com> Co-authored-by: Kevin Bates --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c54f9a1594..8129e42f08 100755 --- a/setup.py +++ b/setup.py @@ -120,7 +120,7 @@ 'jupyter_core>=4.6.1', 'jupyter_client>=5.3.4', 'nbformat', - 'nbconvert', + 'nbconvert>=5', 'nest-asyncio>=1.5', 'ipykernel', # bless IPython kernel for now 'Send2Trash>=1.8.0', From 7742a28e8ec6c8613cf68230039e794e73d11fe7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 14 Mar 2022 06:06:56 -0500 Subject: [PATCH 03/78] Automated Changelog Entry for 6.4.9 on 6.4.x (#6309) Co-authored-by: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0fef4f52f..cbe4d2dfe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,23 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with +## 6.4.9 + +([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...6f73a5722615be297f8e434a24c41041437da622)) + +### Maintenance and upkeep improvements + +- Update 6.4.x branch with some missing commits [#6308](https://github.com/jupyter/notebook/pull/6308) ([@kycutler](https://github.com/kycutler)) +- Specify minimum version of nbconvert required [#6286](https://github.com/jupyter/notebook/pull/6286) ([@adamjstewart](https://github.com/adamjstewart)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/jupyter/notebook/graphs/contributors?from=2022-01-12&to=2022-03-14&type=c)) + +[@adamjstewart](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Aadamjstewart+updated%3A2022-01-12..2022-03-14&type=Issues) | [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ablink1073+updated%3A2022-01-12..2022-03-14&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2022-01-12..2022-03-14&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ajtpio+updated%3A2022-01-12..2022-03-14&type=Issues) | [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Akevin-bates+updated%3A2022-01-12..2022-03-14&type=Issues) | [@kycutler](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Akycutler+updated%3A2022-01-12..2022-03-14&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3AZsailer+updated%3A2022-01-12..2022-03-14&type=Issues) + + + ## 6.4.8 ([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...479902d83a691253e0cff8439a33577e82408317)) @@ -28,8 +45,6 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with [@Vishwajeet0510](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3AVishwajeet0510+updated%3A2022-01-12..2022-01-25&type=Issues) - - ## 6.4.7 ([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.6...b77b5e38b8fa1a20150d7fa4d735dbf1c4f00418)) From 9e3a7001117e64a24ead07b888bd055fdd66faf3 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 14 Mar 2022 07:46:27 -0500 Subject: [PATCH 04/78] Update links and fix check-release (#6310) --- .github/workflows/check-release.yml | 1 + CHANGELOG.md | 2 +- README.md | 8 ++++---- docs-translations/hi-IN/README.md | 10 +++++----- docs-translations/ja-JP/README.md | 8 ++++---- docs-translations/ko-KR/README.md | 8 ++++---- docs-translations/zh-CN/README.md | 10 +++++----- docs/doc-requirements.txt | 1 + docs/environment.yml | 1 + docs/source/config_overview.rst | 4 ++-- docs/source/examples/Notebook/examples_index.rst | 2 +- docs/source/links.txt | 4 ++-- docs/source/notebook.rst | 2 +- docs/source/template.tpl | 2 +- notebook/static/notebook/js/notebook.js | 2 +- setup.py | 3 ++- 16 files changed, 36 insertions(+), 32 deletions(-) diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index be28522243..14d6186a42 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -15,6 +15,7 @@ jobs: strategy: matrix: group: [check_release, link_check] + fail-fast: false steps: - name: Checkout uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe4d2dfe5..1f70f4cb66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1349,7 +1349,7 @@ instructions. recursively, unlike normal config values, to make it load more consistently with frontend extensions([2108](https://github.com/jupyter/notebook/pull/2108)). - The notebook server now supports the [bundler - API](https://jupyter-notebook.readthedocs.io/en/latest/extending/bundler_extensions.html) + API](https://jupyter-notebook.readthedocs.io/en/stable/extending/bundler_extensions.html) from the [jupyter_cms incubator project](https://github.com/jupyter-incubator/contentmanagement) ([1579](https://github.com/jupyter/notebook/pull/1579)). - The notebook server now provides information about kernel activity diff --git a/README.md b/README.md index 8f6c2c7491..a69283458d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) [![Build Status](https://travis-ci.org/jupyter/notebook.svg?branch=master)](https://travis-ci.org/jupyter/notebook) -[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/stable/?badge=latest) [![codecov](https://codecov.io/gh/jupyter/notebook/branch/master/graph/badge.svg)](https://codecov.io/gh/jupyter/notebook) The Jupyter notebook is a web-based notebook environment for interactive @@ -39,7 +39,7 @@ discrete repos. You can find the installation documentation for the [Jupyter platform, on ReadTheDocs](https://jupyter.readthedocs.io/en/latest/install.html). The documentation for advanced usage of Jupyter notebook can be found -[here](https://jupyter-notebook.readthedocs.io/en/latest/). +[here](https://jupyter-notebook.readthedocs.io/en/stable/). For a local installation, make sure you have [pip installed](https://pip.readthedocs.io/en/stable/installing/) and run: @@ -69,8 +69,8 @@ If you are interested in contributing to the project, see [`CONTRIBUTING.rst`](C ## Resources - [Project Jupyter website](https://jupyter.org) - [Online Demo at jupyter.org/try](https://jupyter.org/try) -- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)] +- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/stable/) - [Korean Version of Installation](https://github.com/ChungJooHo/Jupyter_Kor_doc/) -- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] +- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html ) - [Issues](https://github.com/jupyter/notebook/issues) - [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) diff --git a/docs-translations/hi-IN/README.md b/docs-translations/hi-IN/README.md index e234e3dac7..1e85490e66 100644 --- a/docs-translations/hi-IN/README.md +++ b/docs-translations/hi-IN/README.md @@ -2,7 +2,7 @@ [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) [![Build Status](https://travis-ci.org/jupyter/notebook.svg?branch=master)](https://travis-ci.org/jupyter/notebook) -[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/stable/?badge=latest) @@ -40,7 +40,7 @@ IPython कोडबेस का बिग स्प्लिट ™। IPython आप के लिए स्थापना प्रलेखन पा सकते हैं [बृहस्पति मंच, ReadTheDocs पर](https://jupyter.readthedocs.io/en/latest/install.html). जुपिटर नोटबुक के उन्नत उपयोग के लिए दस्तावेज पाया जा सकता है -[यहाँ](https://jupyter-notebook.readthedocs.io/en/latest/). +[यहाँ](https://jupyter-notebook.readthedocs.io/en/stable/). स्थानीय स्थापना के लिए, सुनिश्चित करें कि आपके पास है [pip स्थापित](https://pip.readthedocs.io/en/stable/installing/) और भाग खड़ा हुआ: @@ -70,8 +70,8 @@ IPython कोडबेस का बिग स्प्लिट ™। IPython ## साधन - [Project Jupyter website](https://jupyter.org) - [Online Demo at jupyter.org/try](https://jupyter.org/try) -- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)] +- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/stable/) - [Korean Version of Installation](https://github.com/ChungJooHo/Jupyter_Kor_doc/) -- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] +- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html ) - [Issues](https://github.com/jupyter/notebook/issues) -- [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) \ No newline at end of file +- [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) diff --git a/docs-translations/ja-JP/README.md b/docs-translations/ja-JP/README.md index 4a344ce26e..9262cdbecc 100644 --- a/docs-translations/ja-JP/README.md +++ b/docs-translations/ja-JP/README.md @@ -2,7 +2,7 @@ [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) [![Build Status](https://travis-ci.org/jupyter/notebook.svg?branch=master)](https://travis-ci.org/jupyter/notebook) -[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/stable/?badge=latest) 英語版のリンク : [[English Version](http://github.com/jupyter/notebook/)] @@ -21,7 +21,7 @@ Jupyter Notebookは、Project Jupyter用の言語に依存しないHTMLノート ## インストール [Jupyter platform, on ReadTheDocs](https://jupyter.readthedocs.io/en/latest/install.html)から、インストールドキュメントをご覧になれます。 -Jupyter Notebookの発展的な使用方法に関するドキュメントは、[こちら](https://jupyter-notebook.readthedocs.io/en/latest/)をご覧ください。 +Jupyter Notebookの発展的な使用方法に関するドキュメントは、[こちら](https://jupyter-notebook.readthedocs.io/en/stable/)をご覧ください。 ローカルへのインストールの場合、[pip](https://pip.readthedocs.io/en/stable/installing/)をインストールしていることを確認し、以下のコマンドを実行してください。 @@ -51,7 +51,7 @@ Jupyter Notebookをリモートで起動する前に、いくつかの構成が - [Project Jupyter website](https://jupyter.org) - [Online Demo at try.jupyter.org](https://try.jupyter.org) -- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)] -- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] +- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/stable/) +- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html ) - [Issues](https://github.com/jupyter/notebook/issues) - [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) diff --git a/docs-translations/ko-KR/README.md b/docs-translations/ko-KR/README.md index 4fad2a8dac..a54b996648 100644 --- a/docs-translations/ko-KR/README.md +++ b/docs-translations/ko-KR/README.md @@ -2,7 +2,7 @@ [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) [![Build Status](https://travis-ci.org/jupyter/notebook.svg?branch=master)](https://travis-ci.org/jupyter/notebook) -[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](http://jupyter-notebook.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](http://jupyter-notebook.readthedocs.io/en/stable/?badge=stable) English 버전 링크 : [[English Version](http://github.com/jupyter/notebook/)] @@ -23,7 +23,7 @@ IPython 3는 *IPython notebook* 과 같은 사용자 언어에 독립적인 코 You can find the installation documentation for the [Jupyter platform, on ReadTheDocs](https://jupyter.readthedocs.io/en/latest/install.html). 조금 더 심화된 Jupyter notebook의 사용은 다음 주소에서 볼 수 있습니다. -[here](https://jupyter-notebook.readthedocs.io/en/latest/). +[here](https://jupyter-notebook.readthedocs.io/en/stable/). 설치를 위해서는 [pip installed](https://pip.readthedocs.io/en/stable/installing/) 가 있는지 확인한 후 다음을 실행해주세요: @@ -49,7 +49,7 @@ You can find the installation documentation for the ## 자료 - [Project Jupyter website](https://jupyter.org) - [Online Demo at try.jupyter.org](https://try.jupyter.org) -- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)] -- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] +- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/stable/) +- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html ) - [Issues](https://github.com/jupyter/notebook/issues) - [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) diff --git a/docs-translations/zh-CN/README.md b/docs-translations/zh-CN/README.md index 13da7f722b..5bee3f7d4c 100644 --- a/docs-translations/zh-CN/README.md +++ b/docs-translations/zh-CN/README.md @@ -2,7 +2,7 @@ [![Google Group](https://img.shields.io/badge/-Google%20Group-lightgrey.svg)](https://groups.google.com/forum/#!forum/jupyter) [![Build Status](https://travis-ci.org/jupyter/notebook.svg?branch=master)](https://travis-ci.org/jupyter/notebook) -[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/latest/?badge=latest) +[![Documentation Status](https://readthedocs.org/projects/jupyter-notebook/badge/?version=latest)](https://jupyter-notebook.readthedocs.io/en/stable/?badge=latest) @@ -40,7 +40,7 @@ IPython代码库的Big Split™。 IPython 3是最后一个主要的整体 您可以找到以下安装文件 [Jupiter论坛,在ReadTheDocs上](https://jupyter.readthedocs.io/en/latest/install.html). 可以找到有关Jupiter笔记本的高级使用的文档 -[这里](https://jupyter-notebook.readthedocs.io/en/latest/). +[这里](https://jupyter-notebook.readthedocs.io/en/stable/). 对于本地安装,请确保您已经 [pip 成立时间](https://pip.readthedocs.io/en/stable/installing/) 并运行: @@ -70,8 +70,8 @@ IPython代码库的Big Split™。 IPython 3是最后一个主要的整体 ## 资源 - [Project Jupyter website](https://jupyter.org) - [Online Demo at jupyter.org/try](https://jupyter.org/try) -- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/latest/) [[PDF](https://media.readthedocs.org/pdf/jupyter-notebook/latest/jupyter-notebook.pdf)] +- [Documentation for Jupyter notebook](https://jupyter-notebook.readthedocs.io/en/stable/) - [Korean Version of Installation](https://github.com/ChungJooHo/Jupyter_Kor_doc/) -- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html) [[PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf)] +- [Documentation for Project Jupyter](https://jupyter.readthedocs.io/en/latest/index.html ) - [Issues](https://github.com/jupyter/notebook/issues) -- [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) \ No newline at end of file +- [Technical support - Jupyter Google Group](https://groups.google.com/forum/#!forum/jupyter) diff --git a/docs/doc-requirements.txt b/docs/doc-requirements.txt index bb439ddcf2..3b6551624c 100644 --- a/docs/doc-requirements.txt +++ b/docs/doc-requirements.txt @@ -3,3 +3,4 @@ sphinx-rtd-theme nbsphinx sphinxcontrib_github_alt myst_parser +ipython_genutils diff --git a/docs/environment.yml b/docs/environment.yml index 52ef4df581..55cea9b26c 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -18,3 +18,4 @@ dependencies: - Send2Trash - prometheus_client - sphinxcontrib_github_alt + - ipython_genutils diff --git a/docs/source/config_overview.rst b/docs/source/config_overview.rst index ccb64a2751..96356d07aa 100644 --- a/docs/source/config_overview.rst +++ b/docs/source/config_overview.rst @@ -64,8 +64,8 @@ Notebook front-end client Notebook extensions ------------------- -- `Distributing Jupyter Extensions as Python Packages `_ -- `Extending the Notebook `_ +- `Distributing Jupyter Extensions as Python Packages `_ +- `Extending the Notebook `_ :ref:`Security in Jupyter notebooks: ` Since security diff --git a/docs/source/examples/Notebook/examples_index.rst b/docs/source/examples/Notebook/examples_index.rst index 61bddadd07..9339074573 100644 --- a/docs/source/examples/Notebook/examples_index.rst +++ b/docs/source/examples/Notebook/examples_index.rst @@ -5,7 +5,7 @@ Notebook Examples The pages in this section are all converted notebook files. You can also `view these notebooks on nbviewer`__. -__ https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/ +__ https://nbviewer.jupyter.org/github/jupyter/notebook/blob/6.4.x/ docs/source/examples/Notebook/ .. toctree:: diff --git a/docs/source/links.txt b/docs/source/links.txt index 0306b047fb..f6503b7544 100644 --- a/docs/source/links.txt +++ b/docs/source/links.txt @@ -19,7 +19,7 @@ .. Main Jupyter notebook links .. _Notebook Basics: notebook_p2_ -.. _notebook_p2: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Notebook%20Basics.ipynb +.. _notebook_p2: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/6.4.x/docs/source/examples/Notebook/Notebook%20Basics.ipynb .. _Running Code in the Jupyter Notebook: notebook_p1_ .. _notebook_p1: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Running%20Code.ipynb @@ -38,4 +38,4 @@ .. _Plotting with Matplotlib: notebook_p3_ .. _notebook_p3: https://nbviewer.jupyter.org/github/ipython/ipython/blob/master/examples/IPython%20Kernel/Plotting%20in%20the%20Notebook.ipynb -.. _Working with Markdown Cells: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/master/docs/source/examples/Notebook/Working%20With%20Markdown%20Cells.ipynb +.. _Working with Markdown Cells: https://nbviewer.jupyter.org/github/jupyter/notebook/blob/6.4.x/docs/source/examples/Notebook/Working%20With%20Markdown%20Cells.ipynb diff --git a/docs/source/notebook.rst b/docs/source/notebook.rst index fb1e0195c5..f5db5f95b2 100644 --- a/docs/source/notebook.rst +++ b/docs/source/notebook.rst @@ -234,7 +234,7 @@ drop-down on the toolbar (which will be "Code", initially), or via For more information on the different things you can do in a notebook, see the `collection of examples -`_. +`_. Code cells ~~~~~~~~~~ diff --git a/docs/source/template.tpl b/docs/source/template.tpl index 411ef7c68d..8c9d4af9e9 100644 --- a/docs/source/template.tpl +++ b/docs/source/template.tpl @@ -2,7 +2,7 @@ {% macro notebooklink() -%} -`View the original notebook on nbviewer `__ +`View the original notebook on nbviewer `__ {%- endmacro %} diff --git a/notebook/static/notebook/js/notebook.js b/notebook/static/notebook/js/notebook.js index 606851b965..967362ab51 100644 --- a/notebook/static/notebook/js/notebook.js +++ b/notebook/static/notebook/js/notebook.js @@ -2992,7 +2992,7 @@ define([ .text(i18n.msg._("A trusted Jupyter notebook may execute hidden malicious code when you open it. " + "Selecting trust will immediately reload this notebook in a trusted state. " + "For more information, see the Jupyter security documentation: ")) - .append($("").attr("href", "https://jupyter-notebook.readthedocs.io/en/latest/security.html") + .append($("").attr("href", "https://jupyter-notebook.readthedocs.io/en/stable/security.html") .text(i18n.msg._("here")) ) ); diff --git a/setup.py b/setup.py index 8129e42f08..2f83bb017a 100755 --- a/setup.py +++ b/setup.py @@ -85,6 +85,7 @@ Read `the documentation `_ for more information. """, + long_description_content_type = ' text/markdown', version = version, packages = find_packages(), package_data = find_package_data(), @@ -115,10 +116,10 @@ # but hopefully avoids incompatibilities with Tornado 5. April 2018 'pyzmq>=17', 'argon2-cffi', - 'ipython_genutils', 'traitlets>=4.2.1', 'jupyter_core>=4.6.1', 'jupyter_client>=5.3.4', + 'ipython_genutils', 'nbformat', 'nbconvert>=5', 'nest-asyncio>=1.5', From 8dd601093a057215745ee195b8f79599c5685cc1 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 14 Mar 2022 08:22:45 -0500 Subject: [PATCH 05/78] Fix long_description_content_type --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2f83bb017a..e255a3129a 100755 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ Read `the documentation `_ for more information. """, - long_description_content_type = ' text/markdown', + long_description_content_type = 'text/markdown', version = version, packages = find_packages(), package_data = find_package_data(), From 3f2864df86d73f4f1272ded52e40ea1825343243 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 14 Mar 2022 08:23:17 -0500 Subject: [PATCH 06/78] Automated Changelog Entry for 6.4.9 on 6.4.x (#6312) Co-authored-by: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f70f4cb66..e02ff8e801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,15 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with ## 6.4.9 -([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...6f73a5722615be297f8e434a24c41041437da622)) +([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...9e3a7001117e64a24ead07b888bd055fdd66faf3)) ### Maintenance and upkeep improvements +- Update links and fix check-release [#6310](https://github.com/jupyter/notebook/pull/6310) ([@blink1073](https://github.com/blink1073)) - Update 6.4.x branch with some missing commits [#6308](https://github.com/jupyter/notebook/pull/6308) ([@kycutler](https://github.com/kycutler)) + +### Other merged PRs + - Specify minimum version of nbconvert required [#6286](https://github.com/jupyter/notebook/pull/6286) ([@adamjstewart](https://github.com/adamjstewart)) ### Contributors to this release From eb463d88beb804585733bd39d5de4050551b1f8e Mon Sep 17 00:00:00 2001 From: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 13:33:51 +0000 Subject: [PATCH 07/78] Publish 6.4.9 SHA256 hashes: notebook-6.4.9-py3-none-any.whl: 14ebcde55100176d677385deabf4b3c60e97d82cab0ae76d93241872bc368274 notebook-6.4.9.tar.gz: 64d898f0efcec7def4607b686e25d175670f51c701966373b2c5d7b38281d522 --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index 6c5729f54d..72ef7a6462 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.5.0.dev0' +__version__ = '6.4.9' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 2705bedebf..87a2fd5ec7 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.4.8"; + Jupyter.version = "6.4.9"; Jupyter._target = '_blank'; return Jupyter; diff --git a/pyproject.toml b/pyproject.toml index 9553029ced..c1b77bdd4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.5.0.dev0" +current = "6.4.9" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From 1212e0fec45d6ca1071565a8fa3e4add02ce3cc4 Mon Sep 17 00:00:00 2001 From: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Mar 2022 13:33:59 +0000 Subject: [PATCH 08/78] Bump to 6.5.0.dev0 --- notebook/_version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index 72ef7a6462..6c5729f54d 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.4.9' +__version__ = '6.5.0.dev0' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' diff --git a/pyproject.toml b/pyproject.toml index c1b77bdd4b..9553029ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.4.9" +current = "6.5.0.dev0" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From c219ce43c1ea25123fa70d264e7735bdf4585b1e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 15 Mar 2022 10:42:52 -0500 Subject: [PATCH 09/78] Merge pull request from GHSA-m87f-39q9-6f55 --- notebook/log.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/notebook/log.py b/notebook/log.py index eb246bc400..f7e393770b 100644 --- a/notebook/log.py +++ b/notebook/log.py @@ -44,11 +44,15 @@ def log_request(handler, log=access_log, log_json=False): ns['referer'] = request.headers.get('Referer', 'None') msg = msg + ' referer={referer}' if status >= 500 and status != 502: - # log all headers if it caused an error + # Log a subset of the headers if it caused an error. + headers = {} + for header in ['Host', 'Accept', 'Referer', 'User-Agent']: + if header in request.headers: + headers[header] = request.headers[header] if log_json: - log_method("", extra=dict(props=dict(request.headers))) + log_method("", extra=dict(props=headers)) else: - log_method(json.dumps(dict(request.headers), indent=2)) + log_method(json.dumps(dict(headers), indent=2)) if log_json: log_method("", extra=dict(props=ns)) else: From 5beec5484a1e08b8c53a5c7e416052b21d056f83 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 15 Mar 2022 10:55:14 -0500 Subject: [PATCH 10/78] Release 6.4.10 --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index 6c5729f54d..d9dadc3254 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.5.0.dev0' +__version__ = '6.4.10' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 87a2fd5ec7..64e28db7e6 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.4.9"; + Jupyter.version = "6.4.10"; Jupyter._target = '_blank'; return Jupyter; diff --git a/pyproject.toml b/pyproject.toml index 9553029ced..c6f66a60e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.5.0.dev0" +current = "6.4.10" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From 232f724c64ddf84d8a21d630bde5ea2be3f4b586 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 15 Mar 2022 10:56:55 -0500 Subject: [PATCH 11/78] Back to dev version --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index d9dadc3254..6c5729f54d 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.4.10' +__version__ = '6.5.0.dev0' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 64e28db7e6..5eeb1ebd0d 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.4.10"; + Jupyter.version = "6.5.0.dev0"; Jupyter._target = '_blank'; return Jupyter; diff --git a/pyproject.toml b/pyproject.toml index c6f66a60e6..9553029ced 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.4.10" +current = "6.5.0.dev0" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From 3c54a9a4a41ee1e36f6b11d53feff921a3dd79b7 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 21 Mar 2022 14:50:37 +0100 Subject: [PATCH 12/78] Use place-hold.it instead of plaecehold.it to create image placeholders (#6320) --- tools/tests/Confined Output.ipynb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/tests/Confined Output.ipynb b/tools/tests/Confined Output.ipynb index 8ba354d6ec..42bf5c89bc 100644 --- a/tools/tests/Confined Output.ipynb +++ b/tools/tests/Confined Output.ipynb @@ -13,11 +13,11 @@ "source": [ "markdown image:\n", "\n", - "\n", + "\n", "\n", "unconfined markdown image:\n", "\n", - "" + "" ] }, { @@ -48,7 +48,7 @@ { "data": { "text/html": [ - "" + "" ], "text/plain": [ "" @@ -60,7 +60,7 @@ } ], "source": [ - "Image(url=\"http://placehold.it/800x200.png\", embed=False)" + "Image(url=\"http://place-hold.it/800x200.png\", embed=False)" ] }, { @@ -90,7 +90,7 @@ } ], "source": [ - "Image(url=\"http://placehold.it/800x200.png\", embed=True)" + "Image(url=\"http://place-hold.it/800x200.png\", embed=True)" ] }, { @@ -124,7 +124,7 @@ } ], "source": [ - "Image(url=\"http://placehold.it/800x200.png\", embed=True, unconfined=True)" + "Image(url=\"http://place-hold.it/800x200.png\", embed=True, unconfined=True)" ] }, { @@ -159,7 +159,7 @@ } ], "source": [ - "Image(url=\"http://placehold.it/1800x200.jpg\", embed=True, retina=True)" + "Image(url=\"http://place-hold.it/1800x200.jpg\", embed=True, retina=True)" ] }, { From 4ba07e130f3c0df31b878af99a9b3e8123b69948 Mon Sep 17 00:00:00 2001 From: Alexey Stepanov Date: Mon, 11 Apr 2022 14:01:43 +0200 Subject: [PATCH 13/78] Fix #6256 : migrate to python 3.7+ (#6260) --- docs/source/conf.py | 2 +- notebook/__main__.py | 1 - notebook/_sysinfo.py | 6 +- notebook/auth/__main__.py | 9 +- notebook/auth/login.py | 18 +- notebook/auth/security.py | 11 +- notebook/auth/tests/test_security.py | 10 +- notebook/base/handlers.py | 121 ++++++------ notebook/base/zmqhandlers.py | 6 +- notebook/bundler/bundlerextensions.py | 70 +++---- notebook/bundler/handlers.py | 8 +- notebook/bundler/tarball_bundler.py | 6 +- notebook/bundler/tests/test_bundler_api.py | 17 +- notebook/config_manager.py | 5 +- notebook/edit/handlers.py | 6 +- notebook/extensions.py | 6 +- notebook/gateway/handlers.py | 36 ++-- notebook/gateway/managers.py | 39 ++-- notebook/i18n/__init__.py | 5 +- notebook/jstest.py | 66 ++++--- notebook/kernelspecs/handlers.py | 6 +- notebook/log.py | 6 +- notebook/nbconvert/handlers.py | 19 +- .../tests/test_nbconvert_handlers.py | 42 ++-- notebook/nbextensions.py | 185 +++++++++--------- notebook/notebook/handlers.py | 11 +- notebook/notebookapp.py | 68 ++++--- notebook/prometheus/log_functions.py | 2 +- notebook/serverextensions.py | 28 ++- notebook/services/config/handlers.py | 5 +- .../services/config/tests/test_config_api.py | 4 +- notebook/services/contents/checkpoints.py | 6 +- notebook/services/contents/filecheckpoints.py | 14 +- notebook/services/contents/fileio.py | 25 ++- notebook/services/contents/filemanager.py | 63 +++--- notebook/services/contents/handlers.py | 35 ++-- .../services/contents/largefilemanager.py | 20 +- notebook/services/contents/manager.py | 64 +++--- .../contents/tests/test_contents_api.py | 148 +++++++------- .../services/contents/tests/test_fileio.py | 68 +++---- .../contents/tests/test_largefilemanager.py | 6 +- .../services/contents/tests/test_manager.py | 52 ++--- notebook/services/kernels/handlers.py | 34 ++-- notebook/services/kernels/kernelmanager.py | 18 +- .../kernels/tests/test_kernels_api.py | 13 +- notebook/services/kernelspecs/handlers.py | 4 +- .../kernelspecs/tests/test_kernelspecs_api.py | 31 ++- notebook/services/nbconvert/handlers.py | 2 +- .../nbconvert/tests/test_nbconvert_api.py | 4 +- notebook/services/sessions/handlers.py | 14 +- notebook/services/sessions/sessionmanager.py | 16 +- .../sessions/tests/test_sessionmanager.py | 44 ++--- .../sessions/tests/test_sessions_api.py | 14 +- notebook/terminal/__init__.py | 2 +- notebook/terminal/handlers.py | 12 +- notebook/terminal/terminalmanager.py | 9 +- notebook/terminal/tests/test_terminals_api.py | 2 +- notebook/tests/conftest.py | 4 +- notebook/tests/launchnotebook.py | 14 +- notebook/tests/selenium/conftest.py | 10 +- notebook/tests/selenium/test_dashboard_nav.py | 16 +- notebook/tests/selenium/test_deletecell.py | 7 +- notebook/tests/selenium/test_display_image.py | 6 +- .../tests/selenium/test_display_isolation.py | 18 +- notebook/tests/selenium/test_kernel_menu.py | 3 +- notebook/tests/selenium/test_merge_cells.py | 6 +- .../tests/selenium/test_multiselect_toggle.py | 3 +- .../tests/selenium/test_prompt_numbers.py | 1 - notebook/tests/selenium/test_save.py | 3 +- .../tests/selenium/test_save_as_notebook.py | 4 +- .../tests/selenium/test_save_readonly_as.py | 2 +- notebook/tests/selenium/utils.py | 71 ++++--- notebook/tests/test_files.py | 40 ++-- notebook/tests/test_gateway.py | 19 +- notebook/tests/test_nbextensions.py | 80 ++++---- notebook/tests/test_notebookapp.py | 8 +- .../tests/test_notebookapp_integration.py | 13 +- notebook/tests/test_paths.py | 3 +- notebook/tests/test_serverextensions.py | 13 +- notebook/tests/test_utils.py | 2 +- notebook/traittypes.py | 22 ++- notebook/tree/handlers.py | 2 +- notebook/tree/tests/handlers.py | 2 +- notebook/tree/tests/test_tree_handler.py | 9 +- notebook/utils.py | 8 +- notebook/view/handlers.py | 4 +- setup.py | 8 +- setupbase.py | 50 ++--- tools/secure_notebook.py | 19 +- 89 files changed, 972 insertions(+), 1042 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 1aa7613b42..9482f01205 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -319,7 +319,7 @@ # -- Options for link checks ---------------------------------------------- linkcheck_ignore = [ - 'http://127\.0\.0\.1/*' + r'http://127\.0\.0\.1/*' ] diff --git a/notebook/__main__.py b/notebook/__main__.py index ee339caebf..c11733204c 100644 --- a/notebook/__main__.py +++ b/notebook/__main__.py @@ -1,4 +1,3 @@ - if __name__ == '__main__': from notebook import notebookapp as app app.launch_new_instance() diff --git a/notebook/_sysinfo.py b/notebook/_sysinfo.py index 951ffd1014..4abeadcedb 100644 --- a/notebook/_sysinfo.py +++ b/notebook/_sysinfo.py @@ -55,10 +55,10 @@ def pkg_commit_hash(pkg_path): if repo_commit: return 'repository', repo_commit.strip().decode('ascii') else: - return u'', u'' + return '', '' par_path = p.dirname(par_path) - - return u'', u'' + + return '', '' def pkg_info(pkg_path): diff --git a/notebook/auth/__main__.py b/notebook/auth/__main__.py index ff413b534e..26c0a8f2a0 100644 --- a/notebook/auth/__main__.py +++ b/notebook/auth/__main__.py @@ -25,14 +25,17 @@ def set_password(args): } }) if not args.quiet: - print("password stored in config dir: %s" % jupyter_config_dir()) + print(f"password stored in config dir: {jupyter_config_dir()}") def main(argv): parser = argparse.ArgumentParser(argv[0]) subparsers = parser.add_subparsers() parser_password = subparsers.add_parser('password', help='sets a password for your notebook server') - parser_password.add_argument("password", help="password to set, if not given, a password will be queried for (NOTE: this may not be safe)", - nargs="?") + parser_password.add_argument( + "password", + help="password to set, if not given, a password will be queried for (NOTE: this may not be safe)", + nargs="?" + ) parser_password.add_argument("--quiet", help="suppress messages", action="store_true") parser_password.set_defaults(function=set_password) args = parser.parse_args(argv[1:]) diff --git a/notebook/auth/login.py b/notebook/auth/login.py index 16c4e7a356..47cfb79ae0 100644 --- a/notebook/auth/login.py +++ b/notebook/auth/login.py @@ -48,7 +48,7 @@ def _redirect_safe(self, url, default=None): # OR pass our cross-origin check if parsed.netloc: # if full URL, run our cross-origin check: - origin = '%s://%s' % (parsed.scheme, parsed.netloc) + origin = f'{parsed.scheme}://{parsed.netloc}' origin = origin.lower() if self.allow_origin: allow = self.allow_origin == origin @@ -56,7 +56,7 @@ def _redirect_safe(self, url, default=None): allow = bool(self.allow_origin_pat.match(origin)) if not allow: # not allowed, use default - self.log.warning("Not allowing login redirect to %r" % url) + self.log.warning(f"Not allowing login redirect to {url!r}") url = default self.redirect(url) @@ -73,13 +73,13 @@ def hashed_password(self): def passwd_check(self, a, b): return passwd_check(a, b) - + def post(self): - typed_password = self.get_argument('password', default=u'') - new_password = self.get_argument('new_password', default=u'') + typed_password = self.get_argument('password', default='') + new_password = self.get_argument('new_password', default='') + - if self.get_login_available(self.settings): if self.passwd_check(self.hashed_password, typed_password) and not new_password: self.set_login_cookie(self, uuid.uuid4().hex) @@ -89,7 +89,7 @@ def post(self): config_dir = self.settings.get('config_dir') config_file = os.path.join(config_dir, 'jupyter_notebook_config.json') set_password(new_password, config_file=config_file) - self.log.info("Wrote hashed password to %s" % config_file) + self.log.info(f"Wrote hashed password to {config_file}") else: self.set_status(401) self._render(message={'error': 'Invalid credentials'}) @@ -197,7 +197,7 @@ def get_user(cls, handler): @classmethod def get_user_token(cls, handler): """Identify the user based on a token in the URL or Authorization header - + Returns: - uuid if authenticated - None if not @@ -245,7 +245,7 @@ def password_from_settings(cls, settings): If there is no configured password, an empty string will be returned. """ - return settings.get('password', u'') + return settings.get('password', '') @classmethod def get_login_available(cls, settings): diff --git a/notebook/auth/security.py b/notebook/auth/security.py index cc9723901b..a043b63e30 100644 --- a/notebook/auth/security.py +++ b/notebook/auth/security.py @@ -5,7 +5,6 @@ from contextlib import contextmanager import getpass import hashlib -import io import json import os import random @@ -71,7 +70,7 @@ def passwd(passphrase=None, algorithm='argon2'): return ':'.join((algorithm, cast_unicode(h, 'ascii'))) else: h = hashlib.new(algorithm) - salt = ('%0' + str(salt_len) + 'x') % random.getrandbits(4 * salt_len) + salt = f"{random.getrandbits(4 * salt_len):0{salt_len}x}" h.update(cast_bytes(passphrase, 'utf-8') + str_to_bytes(salt, 'ascii')) return ':'.join((algorithm, salt, h.hexdigest())) @@ -135,7 +134,7 @@ def passwd_check(hashed_passphrase, passphrase): def persist_config(config_file=None, mode=0o600): """Context manager that can be used to modify a config object - On exit of the context manager, the config will be written back to disk, + On exit of the context manager, the config will be written back to disk, by default with user-only (600) permissions. """ @@ -152,20 +151,20 @@ def persist_config(config_file=None, mode=0o600): yield config - with io.open(config_file, 'w', encoding='utf8') as f: + with open(config_file, 'w', encoding='utf8') as f: f.write(cast_unicode(json.dumps(config, indent=2))) try: os.chmod(config_file, mode) except Exception as e: tb = traceback.format_exc() - warnings.warn("Failed to set permissions on %s:\n%s" % (config_file, tb), + warnings.warn(f"Failed to set permissions on {config_file}:\n{tb}", RuntimeWarning) def set_password(password=None, config_file=None): """Ask user for password, store it in notebook json configuration file""" - + hashed_password = passwd(password) with persist_config(config_file) as config: diff --git a/notebook/auth/tests/test_security.py b/notebook/auth/tests/test_security.py index cf748cda82..b042fcbfa9 100644 --- a/notebook/auth/tests/test_security.py +++ b/notebook/auth/tests/test_security.py @@ -18,8 +18,8 @@ def test_bad(): def test_passwd_check_unicode(): # GH issue #4524 - phash = u'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f' - assert passwd_check(phash, u"łe¶ŧ←↓→") - phash = (u'argon2:$argon2id$v=19$m=10240,t=10,p=8$' - u'qjjDiZUofUVVnrVYxacnbA$l5pQq1bJ8zglGT2uXP6iOg') - assert passwd_check(phash, u"łe¶ŧ←↓→") + phash = 'sha1:23862bc21dd3:7a415a95ae4580582e314072143d9c382c491e4f' + assert passwd_check(phash, "łe¶ŧ←↓→") + phash = ('argon2:$argon2id$v=19$m=10240,t=10,p=8$' + 'qjjDiZUofUVVnrVYxacnbA$l5pQq1bJ8zglGT2uXP6iOg') + assert passwd_check(phash, "łe¶ŧ←↓→") diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 216480291d..97c818a342 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -59,7 +59,7 @@ class AuthenticatedHandler(web.RequestHandler): @property def content_security_policy(self): """The default Content-Security-Policy header - + Can be overridden by defining Content-Security-Policy in settings['headers'] """ if 'Content-Security-Policy' in self.settings.get('headers', {}): @@ -132,7 +132,7 @@ def get_current_user(self): def skip_check_origin(self): """Ask my login_handler if I should skip the origin_check - + For example: in the default LoginHandler, if a request is token-authenticated, origin checking should be skipped. """ @@ -152,11 +152,9 @@ def token_authenticated(self): @property def cookie_name(self): - default_cookie_name = non_alphanum.sub('-', 'username-{}'.format( - self.request.host - )) + default_cookie_name = non_alphanum.sub('-', f'username-{self.request.host}') return self.settings.get('cookie_name', default_cookie_name) - + @property def logged_in(self): """Is a user currently logged in?""" @@ -188,14 +186,14 @@ def login_available(self): class IPythonHandler(AuthenticatedHandler): """IPython-specific extensions to authenticated handling - + Mostly property shortcuts to IPython-specific settings. """ @property def ignore_minified_js(self): """Wether to user bundle in template. (*.min files) - + Mainly use for development and avoid file recompilation """ return self.settings.get('ignore_minified_js', False) @@ -203,7 +201,7 @@ def ignore_minified_js(self): @property def config(self): return self.settings.get('config', None) - + @property def log(self): """use the IPython log by default, falling back on tornado's logger""" @@ -213,23 +211,23 @@ def log(self): def jinja_template_vars(self): """User-supplied values to supply to jinja templates.""" return self.settings.get('jinja_template_vars', {}) - + #--------------------------------------------------------------- # URLs #--------------------------------------------------------------- - + @property def version_hash(self): """The version hash to use for cache hints for static files""" return self.settings.get('version_hash', '') - + @property def mathjax_url(self): url = self.settings.get('mathjax_url', '') if not url or url_is_absolute(url): return url return url_path_join(self.base_url, url) - + @property def mathjax_config(self): return self.settings.get('mathjax_config', 'TeX-AMS-MML_HTMLorMML-full,Safe') @@ -251,11 +249,11 @@ def contents_js_source(self): self.log.debug("Using contents: %s", self.settings.get('contents_js_source', 'services/contents')) return self.settings.get('contents_js_source', 'services/contents') - + #--------------------------------------------------------------- # Manager objects #--------------------------------------------------------------- - + @property def kernel_manager(self): return self.settings['kernel_manager'] @@ -263,15 +261,15 @@ def kernel_manager(self): @property def contents_manager(self): return self.settings['contents_manager'] - + @property def session_manager(self): return self.settings['session_manager'] - + @property def terminal_manager(self): return self.settings['terminal_manager'] - + @property def kernel_spec_manager(self): return self.settings['kernel_spec_manager'] @@ -283,22 +281,22 @@ def config_manager(self): #--------------------------------------------------------------- # CORS #--------------------------------------------------------------- - + @property def allow_origin(self): """Normal Access-Control-Allow-Origin""" return self.settings.get('allow_origin', '') - + @property def allow_origin_pat(self): """Regular expression version of allow_origin""" return self.settings.get('allow_origin_pat', None) - + @property def allow_credentials(self): """Whether to set Access-Control-Allow-Credentials""" return self.settings.get('allow_credentials', False) - + def set_default_headers(self): """Add CORS headers, if defined""" super().set_default_headers() @@ -320,19 +318,16 @@ def set_default_headers(self): if self.allow_credentials: self.set_header("Access-Control-Allow-Credentials", 'true') - + def set_attachment_header(self, filename): """Set Content-Disposition: attachment header As a method to ensure handling of filename encoding """ escaped_filename = url_escape(filename) - self.set_header('Content-Disposition', - 'attachment;' - " filename*=utf-8''{utf8}" - .format( - utf8=escaped_filename, - ) + self.set_header( + 'Content-Disposition', + f"attachment; filename*=utf-8''{escaped_filename}" ) def get_origin(self): @@ -424,7 +419,7 @@ def check_referer(self): return True # apply cross-origin checks to Referer: - origin = "{}://{}".format(referer_url.scheme, referer_url.netloc) + origin = f"{referer_url.scheme}://{referer_url.netloc}" if self.allow_origin: allow = self.allow_origin == origin elif self.allow_origin_pat: @@ -453,7 +448,7 @@ def check_xsrf_cookie(self): if not self.check_referer(): referer = self.request.headers.get('Referer') if referer: - msg = "Blocking Cross Origin request from {}.".format(referer) + msg = f"Blocking Cross Origin request from {referer}." else: msg = "Blocking request from unknown origin" raise web.HTTPError(403, msg) from e @@ -505,16 +500,16 @@ def prepare(self): #--------------------------------------------------------------- # template rendering #--------------------------------------------------------------- - + def get_template(self, name): """Return the jinja template object for a given name""" return self.settings['jinja2_env'].get_template(name) - + def render_template(self, name, **ns): ns.update(self.template_namespace) template = self.get_template(name) return template.render(**ns) - + @property def template_namespace(self): return dict( @@ -537,19 +532,19 @@ def template_namespace(self): self.request.headers.get('Accept-Language', ''))), **self.jinja_template_vars ) - + def get_json_body(self): """Return the body of the request as JSON data.""" if not self.request.body: return None # Do we need to call body.decode('utf-8') here? - body = self.request.body.strip().decode(u'utf-8') + body = self.request.body.strip().decode('utf-8') try: model = json.loads(body) except Exception as e: self.log.debug("Bad JSON: %r", body) self.log.error("Couldn't parse JSON", exc_info=True) - raise web.HTTPError(400, u'Invalid JSON in body of request') from e + raise web.HTTPError(400, 'Invalid JSON in body of request') from e return model def write_error(self, status_code, **kwargs): @@ -565,12 +560,12 @@ def write_error(self, status_code, **kwargs): message = exception.log_message % exception.args except Exception: pass - + # construct the custom reason, if defined reason = getattr(exception, 'reason', '') if reason: status_message = reason - + # build template namespace ns = dict( status_code=status_code, @@ -582,7 +577,7 @@ def write_error(self, status_code, **kwargs): self.set_header('Content-Type', 'text/html') # render the template try: - html = self.render_template('%s.html' % status_code, **ns) + html = self.render_template(f'{status_code}.html', **ns) except TemplateNotFound: html = self.render_template('error.html', **ns) @@ -741,15 +736,15 @@ def set_headers(self): # disable browser caching, rely on 304 replies for savings if "v" not in self.request.arguments: self.add_header("Cache-Control", "no-cache") - + def compute_etag(self): return None - + def validate_absolute_path(self, root, absolute_path): """Validate and return the absolute path. - + Requires tornado 3.1 - + Adding to tornado's own handling, forbids the serving of hidden files. """ abs_path = super().validate_absolute_path(root, absolute_path) @@ -762,12 +757,12 @@ def validate_absolute_path(self, root, absolute_path): def json_errors(method): """Decorate methods with this to return GitHub style JSON errors. - + This should be used on any JSON API on any handler method that can raise HTTPErrors. - + This will grab the latest HTTPError exception using sys.exc_info and then: - + 1. Set the HTTP status code based on the HTTPError 2. Create and return a JSON body with a message field describing the error in a human readable form. @@ -793,31 +788,31 @@ def wrapper(self, *args, **kwargs): class FileFindHandler(IPythonHandler, web.StaticFileHandler): """subclass of StaticFileHandler for serving files from a search path""" - + # cache search results, don't search for files more than once _static_paths = {} - + def set_headers(self): super().set_headers() # disable browser caching, rely on 304 replies for savings if "v" not in self.request.arguments or \ any(self.request.path.startswith(path) for path in self.no_cache_paths): self.set_header("Cache-Control", "no-cache") - + def initialize(self, path, default_filename=None, no_cache_paths=None): self.no_cache_paths = no_cache_paths or [] - + if isinstance(path, string_types): path = [path] - + self.root = tuple( os.path.abspath(os.path.expanduser(p)) + os.sep for p in path ) self.default_filename = default_filename - + def compute_etag(self): return None - + @classmethod def get_absolute_path(cls, roots, path): """locate a file to serve on our static file search path""" @@ -826,25 +821,25 @@ def get_absolute_path(cls, roots, path): return cls._static_paths[path] try: abspath = os.path.abspath(filefind(path, roots)) - except IOError: + except OSError: # IOError means not found return '' - + cls._static_paths[path] = abspath - + log().debug("Path %s served from %s"%(path, abspath)) return abspath - + def validate_absolute_path(self, root, absolute_path): """check if the file should be served (raises 404, 403, etc.)""" if absolute_path == '': raise web.HTTPError(404) - + for root in self.root: if (absolute_path + os.sep).startswith(root): break - + return super().validate_absolute_path(root, absolute_path) @@ -874,11 +869,11 @@ def get(self): class FilesRedirectHandler(IPythonHandler): """Handler for redirecting relative URLs to the /files/ handler""" - + @staticmethod def redirect_to_files(self, path): """make redirect logic a reusable static method - + so it can be called from other handlers. """ cm = self.contents_manager @@ -903,7 +898,7 @@ def redirect_to_files(self, path): url = url_path_join(self.base_url, 'files', url_escape(path)) self.log.debug("Redirecting %s to %s", self.request.path, url) self.redirect(url) - + def get(self, path=''): return self.redirect_to_files(self, path) diff --git a/notebook/base/zmqhandlers.py b/notebook/base/zmqhandlers.py index c4d4554a89..85937ef6ec 100644 --- a/notebook/base/zmqhandlers.py +++ b/notebook/base/zmqhandlers.py @@ -87,7 +87,7 @@ def deserialize_binary_message(bmsg): WS_PING_INTERVAL = 30000 -class WebSocketMixin(object): +class WebSocketMixin: """Mixin for common websocket options""" ping_callback = None last_ping = 0 @@ -167,7 +167,7 @@ def open(self, *args, **kwargs): self.send_ping, self.ping_interval, ) self.ping_callback.start() - return super(WebSocketMixin, self).open(*args, **kwargs) + return super().open(*args, **kwargs) def send_ping(self): """send a ping to keep the websocket alive""" @@ -249,7 +249,7 @@ def _on_zmq_reply(self, stream, msg_list): try: msg = self._reserialize_reply(msg_list, channel=channel) except Exception: - self.log.critical("Malformed message: %r" % msg_list, exc_info=True) + self.log.critical(f"Malformed message: {msg_list!r}", exc_info=True) return try: diff --git a/notebook/bundler/bundlerextensions.py b/notebook/bundler/bundlerextensions.py index 2ac346f971..2f218ebdec 100644 --- a/notebook/bundler/bundlerextensions.py +++ b/notebook/bundler/bundlerextensions.py @@ -17,14 +17,14 @@ def _get_bundler_metadata(module): """Gets the list of bundlers associated with a Python package. - + Returns a tuple of (the module, [{ 'name': 'unique name of the bundler', 'label': 'file menu item label for the bundler', 'module_name': 'dotted package/module name containing the bundler', 'group': 'download or deploy parent menu item' }]) - + Parameters ---------- @@ -34,16 +34,16 @@ def _get_bundler_metadata(module): """ m = import_item(module) if not hasattr(m, '_jupyter_bundlerextension_paths'): - raise KeyError('The Python module {} does not contain a valid bundlerextension'.format(module)) + raise KeyError(f'The Python module {module} does not contain a valid bundlerextension') bundlers = m._jupyter_bundlerextension_paths() return m, bundlers def _set_bundler_state(name, label, module_name, group, state, user=True, sys_prefix=False, logger=None): """Set whether a bundler is enabled or disabled. - + Returns True if the final state is the one requested. - + Parameters ---------- name : string @@ -68,14 +68,10 @@ def _set_bundler_state(name, label, module_name, group, state, config_dir = os.path.join( _get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig') cm = BaseJSONConfigManager(config_dir=config_dir) - + if logger: - logger.info("{} {} bundler {}...".format( - "Enabling" if state else "Disabling", - name, - module_name - )) - + logger.info(f"{'Enabling' if state else 'Disabling'} {name} bundler {module_name}...") + if state: cm.update(BUNDLER_SECTION, { BUNDLER_SUBSECTION: { @@ -96,13 +92,13 @@ def _set_bundler_state(name, label, module_name, group, state, return (cm.get(BUNDLER_SECTION) .get(BUNDLER_SUBSECTION, {}) .get(name) is not None) == state - + def _set_bundler_state_python(state, module, user, sys_prefix, logger=None): """Enables or disables bundlers defined in a Python package. - + Returns a list of whether the state was achieved for each bundler. - + Parameters ---------- state : Bool @@ -129,9 +125,9 @@ def _set_bundler_state_python(state, module, user, sys_prefix, logger=None): def enable_bundler_python(module, user=True, sys_prefix=False, logger=None): """Enables bundlers defined in a Python package. - + Returns whether each bundle defined in the packaged was enabled or not. - + Parameters ---------- module : str @@ -147,12 +143,12 @@ def enable_bundler_python(module, user=True, sys_prefix=False, logger=None): """ return _set_bundler_state_python(True, module, user, sys_prefix, logger=logger) - + def disable_bundler_python(module, user=True, sys_prefix=False, logger=None): """Disables bundlers defined in a Python package. - + Returns whether each bundle defined in the packaged was enabled or not. - + Parameters ---------- module : str @@ -176,13 +172,13 @@ class ToggleBundlerExtensionApp(BaseExtensionApp): description = "Enable/disable a bundlerextension in configuration." user = Bool(True, config=True, help="Apply the configuration only for the current user (default)") - + _toggle_value = None - + def _config_file_name_default(self): """The default config file name.""" return 'jupyter_notebook_config' - + def toggle_bundler_python(self, module): """Toggle some extensions in an importable Python module. @@ -210,25 +206,25 @@ def start(self): if self.python: self.toggle_bundler_python(self.extra_args[0]) else: - raise NotImplementedError('Cannot install bundlers from non-Python packages') + raise NotImplementedError('Cannot install bundlers from non-Python packages') class EnableBundlerExtensionApp(ToggleBundlerExtensionApp): """An App that enables bundlerextensions""" name = "jupyter bundlerextension enable" description = """ Enable a bundlerextension in frontend configuration. - + Usage jupyter bundlerextension enable [--system|--sys-prefix] """ _toggle_value = True - + class DisableBundlerExtensionApp(ToggleBundlerExtensionApp): """An App that disables bundlerextensions""" name = "jupyter bundlerextension disable" description = """ Disable a bundlerextension in frontend configuration. - + Usage jupyter bundlerextension disable [--system|--sys-prefix] """ @@ -240,15 +236,15 @@ class ListBundlerExtensionApp(BaseExtensionApp): name = "jupyter nbextension list" version = __version__ description = "List all nbextensions known by the configuration system" - + def list_nbextensions(self): """List all the nbextensions""" config_dirs = [os.path.join(p, 'nbconfig') for p in jupyter_config_path()] - + print("Known bundlerextensions:") - + for config_dir in config_dirs: - head = u' config dir: {}'.format(config_dir) + head = f' config dir: {config_dir}' head_shown = False cm = BaseJSONConfigManager(parent=self, config_dir=config_dir) @@ -258,18 +254,16 @@ def list_nbextensions(self): # only show heading if there is an nbextension here print(head) head_shown = True - + for bundler_id, info in data['bundlerextensions'].items(): label = info.get('label') module = info.get('module_name') if label is None or module is None: - msg = u' {} {}'.format(bundler_id, RED_DISABLED) + msg = f' {bundler_id} {RED_DISABLED}' else: - msg = u' "{}" from {} {}'.format( - label, module, GREEN_ENABLED - ) + msg = f' "{label}" from {module} {GREEN_ENABLED}' print(msg) - + def start(self): """Perform the App's functions as configured""" self.list_nbextensions() @@ -299,7 +293,7 @@ def start(self): # The above should have called a subcommand and raised NoStart; if we # get here, it didn't, so we should self.log.info a message. subcmds = ", ".join(sorted(self.subcommands)) - sys.exit("Please supply at least one subcommand: %s" % subcmds) + sys.exit(f"Please supply at least one subcommand: {subcmds}") main = BundlerExtensionApp.launch_instance diff --git a/notebook/bundler/handlers.py b/notebook/bundler/handlers.py index 868dd78324..213ea8aa84 100644 --- a/notebook/bundler/handlers.py +++ b/notebook/bundler/handlers.py @@ -60,8 +60,7 @@ def get(self, path): try: bundler = self.get_bundler(bundler_id) except KeyError as e: - raise web.HTTPError(400, 'Bundler %s not enabled' % - bundler_id) from e + raise web.HTTPError(400, f'Bundler {bundler_id} not enabled') from e module_name = bundler['module_name'] try: @@ -70,12 +69,11 @@ def get(self, path): except UnicodeEncodeError: # Encode unicode as utf-8 in python2 else import_item fails module_name = module_name.encode('utf-8') - + try: bundler_mod = import_item(module_name) except ImportError as e: - raise web.HTTPError(500, 'Could not import bundler %s ' % - bundler_id) from e + raise web.HTTPError(500, f'Could not import bundler {bundler_id} ') from e # Let the bundler respond in any way it sees fit and assume it will # finish the request diff --git a/notebook/bundler/tarball_bundler.py b/notebook/bundler/tarball_bundler.py index c112841482..74861e7a78 100644 --- a/notebook/bundler/tarball_bundler.py +++ b/notebook/bundler/tarball_bundler.py @@ -20,7 +20,7 @@ def _jupyter_bundlerextension_paths(): def bundle(handler, model): """Create a compressed tarball containing the notebook document. - + Parameters ---------- handler : tornado.web.RequestHandler @@ -31,8 +31,8 @@ def bundle(handler, model): notebook_filename = model['name'] notebook_content = nbformat.writes(model['content']).encode('utf-8') notebook_name = os.path.splitext(notebook_filename)[0] - tar_filename = '{}.tar.gz'.format(notebook_name) - + tar_filename = f'{notebook_name}.tar.gz' + info = tarfile.TarInfo(notebook_filename) info.size = len(notebook_content) diff --git a/notebook/bundler/tests/test_bundler_api.py b/notebook/bundler/tests/test_bundler_api.py index 6c251e5e99..0192160241 100644 --- a/notebook/bundler/tests/test_bundler_api.py +++ b/notebook/bundler/tests/test_bundler_api.py @@ -3,7 +3,6 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import io from os.path import join as pjoin from notebook.tests.launchnotebook import NotebookTestBase @@ -14,7 +13,7 @@ from unittest.mock import patch - + def bundle(handler, model): """Bundler test stub. Echo the notebook path.""" handler.finish(model['path']) @@ -25,17 +24,17 @@ class BundleAPITest(NotebookTestBase): def setup_class(cls): """Make a test notebook. Borrowed from nbconvert test. Assumes the class teardown will clean it up in the end.""" - super(BundleAPITest, cls).setup_class() + super().setup_class() nbdir = cls.notebook_dir nb = new_notebook() - nb.cells.append(new_markdown_cell(u'Created by test')) - cc1 = new_code_cell(source=u'print(2*6)') - cc1.outputs.append(new_output(output_type="stream", text=u'12')) + nb.cells.append(new_markdown_cell('Created by test')) + cc1 = new_code_cell(source='print(2*6)') + cc1.outputs.append(new_output(output_type="stream", text='12')) nb.cells.append(cc1) - - with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w', + + with open(pjoin(nbdir, 'testnb.ipynb'), 'w', encoding='utf-8') as f: write(nb, f, version=4) @@ -68,7 +67,7 @@ def test_bundler_import_error(self): mock.assert_called_with('fake_bundler') self.assertEqual(resp.status_code, 500) self.assertIn('Could not import bundler fake_bundler', resp.text) - + def test_bundler_invoke(self): """Should respond with 200 and output from test bundler stub""" with patch('notebook.bundler.handlers.BundlerHandler.get_bundler') as mock: diff --git a/notebook/config_manager.py b/notebook/config_manager.py index 137cbd691c..a8e4ee26b5 100644 --- a/notebook/config_manager.py +++ b/notebook/config_manager.py @@ -5,7 +5,6 @@ import errno import glob -import io import json import os import copy @@ -97,7 +96,7 @@ def get(self, section_name, include_root=True): data = {} for path in paths: if os.path.isfile(path): - with io.open(path, encoding='utf-8') as f: + with open(path, encoding='utf-8') as f: recursive_update(data, json.load(f)) return data @@ -117,7 +116,7 @@ def set(self, section_name, data): # in order to avoid writing half-finished corrupted data to disk. json_content = json.dumps(data, indent=2) - f = io.open(filename, 'w', encoding='utf-8') + f = open(filename, 'w', encoding='utf-8') with f: f.write(json_content) diff --git a/notebook/edit/handlers.py b/notebook/edit/handlers.py index 9ed9a9c380..2a22337adc 100644 --- a/notebook/edit/handlers.py +++ b/notebook/edit/handlers.py @@ -13,7 +13,7 @@ class EditorHandler(IPythonHandler): def get(self, path): path = path.strip('/') if not self.contents_manager.file_exists(path): - raise web.HTTPError(404, u'File does not exist: %s' % path) + raise web.HTTPError(404, f'File does not exist: {path}') basename = path.rsplit('/', 1)[-1] self.write(self.render_template('edit.html', @@ -24,5 +24,5 @@ def get(self, path): ) default_handlers = [ - (r"/edit%s" % path_regex, EditorHandler), -] \ No newline at end of file + (fr"/edit{path_regex}", EditorHandler), +] diff --git a/notebook/extensions.py b/notebook/extensions.py index 8efa65724b..882ca4cfb4 100644 --- a/notebook/extensions.py +++ b/notebook/extensions.py @@ -65,7 +65,11 @@ class BaseExtensionApp(JupyterApp): def _verbose_changed(self): """Warn about verbosity changes""" import warnings - warnings.warn("`verbose` traits of `{}` has been deprecated, has no effects and will be removed in notebook 5.0.".format(type(self).__name__), DeprecationWarning) + warnings.warn( + f"`verbose` traits of `{type(self).__name__}` has been deprecated, " + f"has no effects and will be removed in notebook 5.0.", + DeprecationWarning + ) def _log_format_default(self): """A default format for messages""" diff --git a/notebook/gateway/handlers.py b/notebook/gateway/handlers.py index d774ba39e2..56ade796d5 100644 --- a/notebook/gateway/handlers.py +++ b/notebook/gateway/handlers.py @@ -101,7 +101,7 @@ def write_message(self, message, binary=False): super().write_message(message, binary=binary) elif self.log.isEnabledFor(logging.DEBUG): msg_summary = WebSocketChannelsHandler._get_message_summary(json_decode(utf8(message))) - self.log.debug("Notebook client closed websocket connection - message dropped: {}".format(msg_summary)) + self.log.debug(f"Notebook client closed websocket connection - message dropped: {msg_summary}") def on_close(self): self.log.debug("Closing websocket connection %s", self.request.path) @@ -112,14 +112,16 @@ def on_close(self): def _get_message_summary(message): summary = [] message_type = message['msg_type'] - summary.append('type: {}'.format(message_type)) + summary.append(f'type: {message_type}') if message_type == 'status': - summary.append(', state: {}'.format(message['content']['execution_state'])) + summary.append(f', state: {message["content"]["execution_state"]}') elif message_type == 'error': - summary.append(', {}:{}:{}'.format(message['content']['ename'], - message['content']['evalue'], - message['content']['traceback'])) + summary.append( + f', {message["content"]["ename"]}:' + f'{message["content"]["evalue"]}:' + f'{message["content"]["traceback"]}' + ) else: summary.append(', ...') # don't display potentially sensitive data @@ -146,7 +148,7 @@ def _connect(self, kernel_id): GatewayClient.instance().ws_url, GatewayClient.instance().kernels_endpoint, url_escape(kernel_id), 'channels' ) - self.log.info('Connecting to {}'.format(ws_url)) + self.log.info(f'Connecting to {ws_url}') kwargs = {} kwargs = GatewayClient.instance().load_connection_args(**kwargs) @@ -158,7 +160,7 @@ def _connection_done(self, fut): if not self.disconnected and fut.exception() is None: # prevent concurrent.futures._base.CancelledError self.ws = fut.result() self.retry = 0 - self.log.debug("Connection is ready: ws: {}".format(self.ws)) + self.log.debug(f"Connection is ready: ws: {self.ws}") else: self.log.warning("Websocket connection has been closed via client disconnect or due to error. " "Kernel with ID '{}' may not be terminated on GatewayClient: {}". @@ -172,7 +174,7 @@ def _disconnect(self): elif not self.ws_future.done(): # Cancel pending connection. Since future.cancel() is a noop on tornado, we'll track cancellation locally self.ws_future.cancel() - self.log.debug("_disconnect: future cancelled, disconnected: {}".format(self.disconnected)) + self.log.debug(f"_disconnect: future cancelled, disconnected: {self.disconnected}") @gen.coroutine def _read_messages(self, callback): @@ -183,10 +185,10 @@ def _read_messages(self, callback): try: message = yield self.ws.read_message() except Exception as e: - self.log.error("Exception reading message from websocket: {}".format(e)) # , exc_info=True) + self.log.error(f"Exception reading message from websocket: {e}") # , exc_info=True) if message is None: if not self.disconnected: - self.log.warning("Lost connection to Gateway: {}".format(self.kernel_id)) + self.log.warning(f"Lost connection to Gateway: {self.kernel_id}") break callback(message) # pass back to notebook client (see self.on_open and WebSocketChannelsHandler.open) else: # ws cancelled - stop reading @@ -231,7 +233,7 @@ def _write_message(self, message): if not self.disconnected and self.ws is not None: self.ws.write_message(message) except Exception as e: - self.log.error("Exception writing message to websocket: {}".format(e)) # , exc_info=True) + self.log.error(f"Exception writing message to websocket: {e}") # , exc_info=True) def on_close(self): """Web socket closed event.""" @@ -247,8 +249,10 @@ def get(self, kernel_name, path, include_body=True): ksm = self.kernel_spec_manager kernel_spec_res = yield ksm.get_kernel_spec_resource(kernel_name, path) if kernel_spec_res is None: - self.log.warning("Kernelspec resource '{}' for '{}' not found. Gateway may not support" - " resource serving.".format(path, kernel_name)) + self.log.warning( + f"Kernelspec resource '{path}' for '{kernel_name}' not found. " + f"Gateway may not support resource serving." + ) else: self.set_header("Content-Type", mimetypes.guess_type(path)[0]) self.finish(kernel_spec_res) @@ -258,6 +262,6 @@ def get(self, kernel_name, path, include_body=True): from ..services.kernelspecs.handlers import kernel_name_regex default_handlers = [ - (r"/api/kernels/%s/channels" % _kernel_id_regex, WebSocketChannelsHandler), - (r"/kernelspecs/%s/(?P.*)" % kernel_name_regex, GatewayResourceHandler), + (fr"/api/kernels/{_kernel_id_regex}/channels", WebSocketChannelsHandler), + (fr"/kernelspecs/{kernel_name_regex}/(?P.*)", GatewayResourceHandler), ] diff --git a/notebook/gateway/managers.py b/notebook/gateway/managers.py index b2ae3b30a2..0ec2c41fb6 100644 --- a/notebook/gateway/managers.py +++ b/notebook/gateway/managers.py @@ -46,7 +46,7 @@ def _url_validate(self, proposal): # Ensure value, if present, starts with 'http' if value is not None and len(value) > 0: if not str(value).lower().startswith('http'): - raise TraitError("GatewayClient url must start with 'http': '%r'" % value) + raise TraitError(f"GatewayClient url must start with 'http': '{value!r}'") return value ws_url = Unicode(default_value=None, allow_none=True, config=True, @@ -71,7 +71,7 @@ def _ws_url_validate(self, proposal): # Ensure value, if present, starts with 'ws' if value is not None and len(value) > 0: if not str(value).lower().startswith('ws'): - raise TraitError("GatewayClient ws_url must start with 'ws': '%r'" % value) + raise TraitError(f"GatewayClient ws_url must start with 'ws': '{value!r}'") return value kernels_endpoint_default_value = '/api/kernels' @@ -276,7 +276,7 @@ def init_static_args(self): self._static_args['headers'] = json.loads(self.headers) if 'Authorization' not in self._static_args['headers'].keys(): self._static_args['headers'].update({ - 'Authorization': 'token {}'.format(self.auth_token) + 'Authorization': f'token {self.auth_token}' }) self._static_args['connect_timeout'] = self.connect_timeout self._static_args['request_timeout'] = self.request_timeout @@ -322,19 +322,22 @@ async def gateway_request(endpoint, **kwargs): except ConnectionRefusedError as e: raise web.HTTPError( 503, - "Connection refused from Gateway server url '{}'. Check to be sure the" - " Gateway instance is running.".format(GatewayClient.instance().url) + f"Connection refused from Gateway server url '{GatewayClient.instance().url}'. " + f"Check to be sure the Gateway instance is running." ) from e except HTTPError as e: # This can occur if the host is valid (e.g., foo.com) but there's nothing there. - raise web.HTTPError(e.code, "Error attempting to connect to Gateway server url '{}'. " - "Ensure gateway url is valid and the Gateway instance is running.". - format(GatewayClient.instance().url)) from e + raise web.HTTPError( + e.code, + f"Error attempting to connect to Gateway server url '{GatewayClient.instance().url}'. " + f"Ensure gateway url is valid and the Gateway instance is running." + ) from e except gaierror as e: raise web.HTTPError( 404, - "The Gateway server specified in the gateway_url '{}' doesn't appear to be valid. Ensure gateway " - "url is valid and the Gateway instance is running.".format(GatewayClient.instance().url) + f"The Gateway server specified in the gateway_url '{GatewayClient.instance().url}' " + f"doesn't appear to be valid. " + f"Ensure gateway url is valid and the Gateway instance is running." ) from e return response @@ -392,7 +395,7 @@ async def start_kernel(self, kernel_id=None, path=None, **kwargs): kwargs['cwd'] = self.cwd_for_path(path) kernel_name = kwargs.get('kernel_name', 'python3') kernel_url = self._get_kernel_endpoint_url() - self.log.debug("Request new kernel at: %s" % kernel_url) + self.log.debug(f"Request new kernel at: {kernel_url}") # Let KERNEL_USERNAME take precedent over http_user config option. if os.environ.get('KERNEL_USERNAME') is None and GatewayClient.instance().http_user: @@ -412,12 +415,12 @@ async def start_kernel(self, kernel_id=None, path=None, **kwargs): ) kernel = json_decode(response.body) kernel_id = kernel['id'] - self.log.info("Kernel started: %s" % kernel_id) - self.log.debug("Kernel args: %r" % kwargs) + self.log.info(f"Kernel started: {kernel_id}") + self.log.debug(f"Kernel args: {kwargs!r}") else: kernel = await self.get_kernel(kernel_id) kernel_id = kernel['id'] - self.log.info("Using existing kernel: %s" % kernel_id) + self.log.info(f"Using existing kernel: {kernel_id}") self._kernels[kernel_id] = kernel return kernel_id @@ -431,12 +434,12 @@ async def get_kernel(self, kernel_id=None, **kwargs): The uuid of the kernel. """ kernel_url = self._get_kernel_endpoint_url(kernel_id) - self.log.debug("Request kernel at: %s" % kernel_url) + self.log.debug(f"Request kernel at: {kernel_url}") try: response = await gateway_request(kernel_url, method='GET') except web.HTTPError as error: if error.status_code == 404: - self.log.warn("Kernel not found at: %s" % kernel_url) + self.log.warn(f"Kernel not found at: {kernel_url}") self.remove_kernel(kernel_id) kernel = None else: @@ -611,7 +614,7 @@ async def get_kernel_spec(self, kernel_name, **kwargs): The name of the kernel. """ kernel_spec_url = self._get_kernelspecs_endpoint_url(kernel_name=str(kernel_name)) - self.log.debug("Request kernel spec at: %s" % kernel_spec_url) + self.log.debug(f"Request kernel spec at: {kernel_spec_url}") try: response = await gateway_request(kernel_spec_url, method='GET') except web.HTTPError as error: @@ -640,7 +643,7 @@ async def get_kernel_spec_resource(self, kernel_name, path): The name of the desired resource """ kernel_spec_resource_url = url_path_join(self.base_resource_endpoint, str(kernel_name), str(path)) - self.log.debug("Request kernel spec resource '{}' at: {}".format(path, kernel_spec_resource_url)) + self.log.debug(f"Request kernel spec resource '{path}' at: {kernel_spec_resource_url}") try: response = await gateway_request(kernel_spec_resource_url, method='GET') except web.HTTPError as error: diff --git a/notebook/i18n/__init__.py b/notebook/i18n/__init__.py index 83f65c5fd9..0f836d2eeb 100644 --- a/notebook/i18n/__init__.py +++ b/notebook/i18n/__init__.py @@ -2,7 +2,6 @@ """ from collections import defaultdict import errno -import io import json from os.path import dirname, join as pjoin import re @@ -58,9 +57,9 @@ def parse_accept_lang_header(accept_lang): def load(language, domain='nbjs'): """Load translations from an nbjs.json file""" try: - f = io.open(pjoin(I18N_DIR, language, 'LC_MESSAGES', 'nbjs.json'), + f = open(pjoin(I18N_DIR, language, 'LC_MESSAGES', 'nbjs.json'), encoding='utf-8') - except IOError as e: + except OSError as e: if e.errno != errno.ENOENT: raise return {} diff --git a/notebook/jstest.py b/notebook/jstest.py index 2bb318af31..91c5977b7a 100644 --- a/notebook/jstest.py +++ b/notebook/jstest.py @@ -60,7 +60,7 @@ def run(self): self.buffer.write(chunk) if self.echo: sys.stdout.write(bytes_to_str(chunk)) - + os.close(self.readfd) os.close(self.writefd) @@ -87,7 +87,7 @@ def halt(self): self.join() -class TestController(object): +class TestController: """Run tests in a subprocess """ #: str, test group to be executed. @@ -110,7 +110,7 @@ def __init__(self): def setup(self): """Create temporary directories etc. - + This is only called when we know the test group will be run. Things created here may be cleaned up by self.cleanup(). """ @@ -138,11 +138,11 @@ def wait(self): def print_extra_info(self): """Print extra information about this test run. - + If we're running in parallel and showing the concise view, this is only called if the test group fails. Otherwise, it's called before the test group is started. - + The base implementation does nothing, but it can be overridden by subclasses. """ @@ -155,7 +155,7 @@ def cleanup_process(self): return # Process doesn't exist, or is already dead. try: - print('Cleaning up stale PID: %d' % subp.pid) + print(f'Cleaning up stale PID: {subp.pid}') subp.kill() except: # (OSError, WindowsError) ? # This is just a best effort, if we fail or the process was @@ -193,7 +193,7 @@ def all_js_groups(): class JSController(TestController): """Run CasperJS tests """ - + requirements = ['casperjs'] def __init__(self, section, xunit=True, engine='phantomjs', url=None): @@ -210,7 +210,7 @@ def __init__(self, section, xunit=True, engine='phantomjs', url=None): js_test_dir = get_js_test_dir() includes = '--includes=' + os.path.join(js_test_dir,'util.js') test_cases = os.path.join(js_test_dir, self.section) - self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine] + self.cmd = ['casperjs', 'test', includes, test_cases, f'--engine={self.engine}'] def setup(self): self.ipydir = TemporaryDirectory() @@ -226,8 +226,8 @@ def setup(self): self.dirs.append(self.home) self.dirs.append(self.config_dir) self.dirs.append(self.nbdir) - os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir1', u'sub ∂ir 1a'))) - os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub ∂ir2', u'sub ∂ir 1b'))) + os.makedirs(os.path.join(self.nbdir.name, os.path.join('sub ∂ir1', 'sub ∂ir 1a'))) + os.makedirs(os.path.join(self.nbdir.name, os.path.join('sub ∂ir2', 'sub ∂ir 1b'))) if self.xunit: self.add_xunit() @@ -240,22 +240,22 @@ def setup(self): alive = False if alive: - self.cmd.append("--url=%s" % self.url) + self.cmd.append(f"--url={self.url}") else: - raise Exception('Could not reach "%s".' % self.url) + raise Exception(f'Could not reach "{self.url}".') else: # start the ipython notebook, so we get the port number self.server_port = 0 self._init_server() if self.server_port: - self.cmd.append('--url=http://localhost:%i%s' % (self.server_port, self.base_url)) + self.cmd.append(f'--url=http://localhost:{self.server_port:d}{self.base_url}') else: # don't launch tests if the server didn't start self.cmd = [sys.executable, '-c', 'raise SystemExit(1)'] def add_xunit(self): xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml') - self.cmd.append('--xunit=%s' % xunit_file) + self.cmd.append(f'--xunit={xunit_file}') def launch(self, buffer_output): # If the engine is SlimerJS, we need to buffer the output because @@ -281,7 +281,7 @@ def wait(self, *pargs, **kwargs): return ret def print_extra_info(self): - print("Running tests with notebook directory %r" % self.nbdir.name) + print(f"Running tests with notebook directory {self.nbdir.name!r}") @property def will_run(self): @@ -295,7 +295,7 @@ def _init_server(self): '--no-browser', '--notebook-dir', self.nbdir.name, '--NotebookApp.token=', - '--NotebookApp.base_url=%s' % self.base_url, + f'--NotebookApp.base_url={self.base_url}', ] # ipc doesn't work on Windows, and darwin has crazy-long temp paths, # which run afoul of ipc's maximum path length. @@ -313,11 +313,12 @@ def _init_server(self): ) with patch.dict('os.environ', {'HOME': self.home.name}): runtime_dir = jupyter_runtime_dir() - self.server_info_file = os.path.join(runtime_dir, - 'nbserver-%i.json' % self.server.pid + self.server_info_file = os.path.join( + runtime_dir, + f'nbserver-{self.server.pid}.json' ) self._wait_for_server() - + def _wait_for_server(self): """Wait 30 seconds for the notebook server to start""" for i in range(300): @@ -333,17 +334,18 @@ def _wait_for_server(self): else: return time.sleep(0.1) - print("Notebook server-info file never arrived: %s" % self.server_info_file, + print( + f"Notebook server-info file never arrived: {self.server_info_file}", file=sys.stderr ) - + def _failed_to_start(self): """Notebook server exited prematurely""" captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace') print("Notebook failed to start: ", file=sys.stderr) print(self.server_command) print(captured, file=sys.stderr) - + def _load_server_info(self): """Notebook server started, load connection info from JSON""" with open(self.server_info_file) as f: @@ -374,10 +376,11 @@ def cleanup(self): try: popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT) except TimeoutExpired: - print("Notebook server still running (%s)" % self.server_info_file, + print( + f"Notebook server still running ({self.server_info_file})", file=sys.stderr ) - + self.stream_capturer.halt() TestController.cleanup(self) @@ -399,11 +402,11 @@ def prepare_controllers(options): def do_run(controller, buffer_output=True): """Setup and run a test controller. - + If buffer_output is True, no output is displayed, to avoid it appearing interleaved. In this case, the caller is responsible for displaying test output on failure. - + Returns ------- controller : TestController @@ -443,7 +446,7 @@ def _add(name, value): _add('Platform', inf['platform']) width = max(len(n) for (n,v) in out) - out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out] + out = [f"{n:<{width}}: {v}\n" for (n, v) in out] avail = [] not_avail = [] @@ -468,7 +471,7 @@ def _add(name, value): def run_jstestall(options): """Run the entire Javascript test suite. - + This function constructs TestControllers and runs them in subprocesses. Parameters @@ -552,16 +555,15 @@ def justify(ltext, rtext, width=70, fill='-'): print('_'*70) print('Test suite completed for system with the following information:') print(report()) - took = "Took %.3fs." % t_tests + took = f"Took {t_tests:.3f}s." print('Status: ', end='') if not failed: - print('OK (%d test groups).' % nrunners, took) + print(f'OK ({nrunners} test groups).', took) else: # If anything went wrong, point out what command to rerun manually to # see the actual errors and individual summary failed_sections = [c.section for c in failed] - print('ERROR - {} out of {} test groups failed ({}).'.format(nfail, - nrunners, ', '.join(failed_sections)), took) + print(f'ERROR - {nfail} out of {nrunners} test groups failed ({", ".join(failed_sections)}).', took) print() print('You may wish to rerun these, with:') print(' python -m notebook.jstest', *failed_sections) diff --git a/notebook/kernelspecs/handlers.py b/notebook/kernelspecs/handlers.py index be768b5ab7..b9c6af4b49 100644 --- a/notebook/kernelspecs/handlers.py +++ b/notebook/kernelspecs/handlers.py @@ -15,7 +15,7 @@ def get(self, kernel_name, path, include_body=True): self.root = ksm.get_kernel_spec(kernel_name).resource_dir except KeyError as e: raise web.HTTPError(404, - u'Kernel spec %s not found' % kernel_name) from e + f'Kernel spec {kernel_name} not found') from e self.log.debug("Serving kernel resource from: %s", self.root) return web.StaticFileHandler.get(self, path, include_body=include_body) @@ -24,5 +24,5 @@ def head(self, kernel_name, path): return self.get(kernel_name, path, include_body=False) default_handlers = [ - (r"/kernelspecs/%s/(?P.*)" % kernel_name_regex, KernelSpecResourceHandler), -] \ No newline at end of file + (fr"/kernelspecs/{kernel_name_regex}/(?P.*)", KernelSpecResourceHandler), +] diff --git a/notebook/log.py b/notebook/log.py index f7e393770b..a495db0641 100644 --- a/notebook/log.py +++ b/notebook/log.py @@ -29,20 +29,20 @@ def log_request(handler, log=access_log, log_json=False): log_method = log.warning else: log_method = log.error - + request_time = 1000.0 * request.request_time() ns = dict( status=status, method=request.method, ip=request.remote_ip, uri=request.uri, - request_time=float('%.2f' % request_time), + request_time=float(f'{request_time:.2f}'), ) msg = "{status} {method} {uri} ({ip}) {request_time:f}ms" if status >= 400: # log bad referers ns['referer'] = request.headers.get('Referer', 'None') - msg = msg + ' referer={referer}' + msg += ' referer={referer}' if status >= 500 and status != 502: # Log a subset of the headers if it caused an error. headers = {} diff --git a/notebook/nbconvert/handlers.py b/notebook/nbconvert/handlers.py index 24a3f093fb..c5c8de252a 100644 --- a/notebook/nbconvert/handlers.py +++ b/notebook/nbconvert/handlers.py @@ -61,19 +61,19 @@ def get_exporter(format, **kwargs): try: from nbconvert.exporters.base import get_exporter except ImportError as e: - raise web.HTTPError(500, "Could not import nbconvert: %s" % e) from e + raise web.HTTPError(500, f"Could not import nbconvert: {e}") from e try: Exporter = get_exporter(format) except KeyError as e: # should this be 400? - raise web.HTTPError(404, u"No exporter for format: %s" % format) from e + raise web.HTTPError(404, f"No exporter for format: {format}") from e try: return Exporter(**kwargs) except Exception as e: app_log.exception("Could not construct Exporter: %s", Exporter) - raise web.HTTPError(500, "Could not construct Exporter: %s" % e) from e + raise web.HTTPError(500, f"Could not construct Exporter: {e}") from e class NbconvertFileHandler(IPythonHandler): @@ -132,7 +132,7 @@ def get(self, format, path): ) except Exception as e: self.log.exception("nbconvert failed: %s", e) - raise web.HTTPError(500, "nbconvert failed: %s" % e) from e + raise web.HTTPError(500, f"nbconvert failed: {e}") from e if respond_zip(self, name, output, resources): return @@ -145,7 +145,7 @@ def get(self, format, path): # MIME type if exporter.output_mimetype: self.set_header('Content-Type', - '%s; charset=utf-8' % exporter.output_mimetype) + f'{exporter.output_mimetype}; charset=utf-8') self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.finish(output) @@ -173,7 +173,7 @@ def post(self, format): "config_dir": self.application.settings['config_dir'], }) except Exception as e: - raise web.HTTPError(500, "nbconvert failed: %s" % e) from e + raise web.HTTPError(500, f"nbconvert failed: {e}") from e if respond_zip(self, name, output, resources): return @@ -181,7 +181,7 @@ def post(self, format): # MIME type if exporter.output_mimetype: self.set_header('Content-Type', - '%s; charset=utf-8' % exporter.output_mimetype) + f'{exporter.output_mimetype}; charset=utf-8') self.finish(output) @@ -194,7 +194,6 @@ def post(self, format): default_handlers = [ - (r"/nbconvert/%s" % _format_regex, NbconvertPostHandler), - (r"/nbconvert/%s%s" % (_format_regex, path_regex), - NbconvertFileHandler), + (fr"/nbconvert/{_format_regex}", NbconvertPostHandler), + (fr"/nbconvert/{_format_regex}{path_regex}", NbconvertFileHandler), ] diff --git a/notebook/nbconvert/tests/test_nbconvert_handlers.py b/notebook/nbconvert/tests/test_nbconvert_handlers.py index e5af13c0c4..93b83738c1 100644 --- a/notebook/nbconvert/tests/test_nbconvert_handlers.py +++ b/notebook/nbconvert/tests/test_nbconvert_handlers.py @@ -1,10 +1,8 @@ -import io import json import os from os.path import join as pjoin import shutil -import requests import pytest from notebook.utils import url_path_join from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error @@ -13,8 +11,6 @@ new_notebook, new_markdown_cell, new_code_cell, new_output, ) -from ipython_genutils.testing.decorators import onlyif_cmds_exist - from base64 import encodebytes @@ -25,7 +21,7 @@ def cmd_exists(cmd): return True -class NbconvertAPI(object): +class NbconvertAPI: """Wrapper for nbconvert API calls.""" def __init__(self, request): self.request = request @@ -72,16 +68,16 @@ def cleanup_dir(): nb = new_notebook() - nb.cells.append(new_markdown_cell(u'Created by test ³')) - cc1 = new_code_cell(source=u'print(2*6)') - cc1.outputs.append(new_output(output_type="stream", text=u'12')) + nb.cells.append(new_markdown_cell('Created by test ³')) + cc1 = new_code_cell(source='print(2*6)') + cc1.outputs.append(new_output(output_type="stream", text='12')) cc1.outputs.append(new_output(output_type="execute_result", data={'image/png' : png_green_pixel}, execution_count=1, )) nb.cells.append(cc1) - with io.open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w', + with open(pjoin(nbdir, 'foo', 'testnb.ipynb'), 'w', encoding='utf-8') as f: write(nb, f, version=4) @@ -94,13 +90,13 @@ def cleanup_dir(): def test_from_file(self): r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb') self.assertEqual(r.status_code, 200) - self.assertIn(u'text/html', r.headers['Content-Type']) - self.assertIn(u'Created by test', r.text) - self.assertIn(u'print', r.text) + self.assertIn('text/html', r.headers['Content-Type']) + self.assertIn('Created by test', r.text) + self.assertIn('print', r.text) r = self.nbconvert_api.from_file('python', 'foo', 'testnb.ipynb') - self.assertIn(u'text/x-python', r.headers['Content-Type']) - self.assertIn(u'print(2*6)', r.text) + self.assertIn('text/x-python', r.headers['Content-Type']) + self.assertIn('print(2*6)', r.text) @pytest.mark.skipif( not cmd_exists('pandoc'), @@ -126,8 +122,8 @@ def test_from_file_download(self): ) def test_from_file_zip(self): r = self.nbconvert_api.from_file('latex', 'foo', 'testnb.ipynb', download=True) - self.assertIn(u'application/zip', r.headers['Content-Type']) - self.assertIn(u'.zip', r.headers['Content-Disposition']) + self.assertIn('application/zip', r.headers['Content-Type']) + self.assertIn('.zip', r.headers['Content-Disposition']) @pytest.mark.skipif( not cmd_exists('pandoc'), @@ -138,13 +134,13 @@ def test_from_post(self): r = self.nbconvert_api.from_post(format='html', nbmodel=nbmodel) self.assertEqual(r.status_code, 200) - self.assertIn(u'text/html', r.headers['Content-Type']) - self.assertIn(u'Created by test', r.text) - self.assertIn(u'print', r.text) + self.assertIn('text/html', r.headers['Content-Type']) + self.assertIn('Created by test', r.text) + self.assertIn('print', r.text) r = self.nbconvert_api.from_post(format='python', nbmodel=nbmodel) - self.assertIn(u'text/x-python', r.headers['Content-Type']) - self.assertIn(u'print(2*6)', r.text) + self.assertIn('text/x-python', r.headers['Content-Type']) + self.assertIn('print(2*6)', r.text) @pytest.mark.skipif( not cmd_exists('pandoc'), @@ -154,5 +150,5 @@ def test_from_post_zip(self): nbmodel = self.request('GET', 'api/contents/foo/testnb.ipynb').json() r = self.nbconvert_api.from_post(format='latex', nbmodel=nbmodel) - self.assertIn(u'application/zip', r.headers['Content-Type']) - self.assertIn(u'.zip', r.headers['Content-Disposition']) + self.assertIn('application/zip', r.headers['Content-Type']) + self.assertIn('.zip', r.headers['Content-Disposition']) diff --git a/notebook/nbextensions.py b/notebook/nbextensions.py index aac752cce9..3619f791ef 100644 --- a/notebook/nbextensions.py +++ b/notebook/nbextensions.py @@ -35,7 +35,7 @@ def check_nbextension(files, user=False, prefix=None, nbextensions_dir=None, sys_prefix=False): """Check whether nbextension files have been installed - + Returns True if all files are found, False if any are missing. Parameters @@ -58,11 +58,11 @@ def check_nbextension(files, user=False, prefix=None, nbextensions_dir=None, sys # make sure nbextensions dir exists if not os.path.exists(nbext): return False - + if isinstance(files, string_types): # one file given, turn it into a list files = [files] - + return all(os.path.exists(pjoin(nbext, f)) for f in files) @@ -72,14 +72,14 @@ def install_nbextension(path, overwrite=False, symlink=False, logger=None, sys_prefix=False ): """Install a Javascript extension for the notebook - + Stages files and/or directories into the nbextensions directory. By default, this compares modification time, and only stages files that need updating. If `overwrite` is specified, matching files are purged before proceeding. - + Parameters ---------- - + path : path to file, directory, zip or tarball archive, or URL to install By default, the file will be installed with its base name, so '/path/to/foo' will install to 'nbextensions/foo'. See the destination argument below to change this. @@ -116,14 +116,14 @@ def install_nbextension(path, overwrite=False, symlink=False, nbext = _get_nbextension_dir(user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir) # make sure nbextensions dir exists ensure_dir_exists(nbext) - + # forcing symlink parameter to False if os.symlink does not exist (e.g., on Windows machines running python 2) if not hasattr(os, 'symlink'): symlink = False - + if isinstance(path, (list, tuple)): raise TypeError("path must be a string pointing to a single extension to install; call this function multiple times to install multiple extensions") - + path = cast_unicode_py2(path) if path.startswith(('https://', 'http://')): @@ -134,7 +134,7 @@ def install_nbextension(path, overwrite=False, symlink=False, filename = urlparse(path).path.split('/')[-1] local_path = os.path.join(td, filename) if logger: - logger.info("Downloading: %s -> %s" % (path, local_path)) + logger.info(f"Downloading: {path} -> {local_path}") urlretrieve(path, local_path) # now install from the local copy full_dest = install_nbextension(local_path, overwrite=overwrite, symlink=symlink, @@ -145,7 +145,7 @@ def install_nbextension(path, overwrite=False, symlink=False, if destination: raise ValueError("Cannot give destination for archives") if logger: - logger.info("Extracting: %s -> %s" % (path, nbext)) + logger.info(f"Extracting: {path} -> {nbext}") if path.endswith('.zip'): archive = zipfile.ZipFile(path) @@ -162,7 +162,7 @@ def install_nbextension(path, overwrite=False, symlink=False, full_dest = normpath(pjoin(nbext, destination)) if overwrite and os.path.lexists(full_dest): if logger: - logger.info("Removing: %s" % full_dest) + logger.info(f"Removing: {full_dest}") if os.path.isdir(full_dest) and not os.path.islink(full_dest): shutil.rmtree(full_dest) else: @@ -172,7 +172,7 @@ def install_nbextension(path, overwrite=False, symlink=False, path = os.path.abspath(path) if not os.path.exists(full_dest): if logger: - logger.info("Symlinking: %s -> %s" % (full_dest, path)) + logger.info(f"Symlinking: {full_dest} -> {path}") os.symlink(path, full_dest) elif os.path.isdir(path): path = pjoin(os.path.abspath(path), '') # end in path separator @@ -180,7 +180,7 @@ def install_nbextension(path, overwrite=False, symlink=False, dest_dir = pjoin(full_dest, parent[len(path):]) if not os.path.exists(dest_dir): if logger: - logger.info("Making directory: %s" % dest_dir) + logger.info(f"Making directory: {dest_dir}") os.makedirs(dest_dir) for file_name in files: src = pjoin(parent, file_name) @@ -210,7 +210,7 @@ def install_nbextension_python(module, overwrite=False, symlink=False, dest = nbext['dest'] if logger: - logger.info("Installing %s -> %s" % (src, dest)) + logger.info(f"Installing {src} -> {dest}") full_dest = install_nbextension( src, overwrite=overwrite, symlink=symlink, user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir, @@ -222,16 +222,16 @@ def install_nbextension_python(module, overwrite=False, symlink=False, return full_dests -def uninstall_nbextension(dest, require=None, user=False, sys_prefix=False, prefix=None, +def uninstall_nbextension(dest, require=None, user=False, sys_prefix=False, prefix=None, nbextensions_dir=None, logger=None): """Uninstall a Javascript extension of the notebook - - Removes staged files and/or directories in the nbextensions directory and + + Removes staged files and/or directories in the nbextensions directory and removes the extension from the frontend config. - + Parameters ---------- - + dest : str path to file, directory, zip or tarball archive, or URL to install name the nbextension is installed to. For example, if destination is 'foo', then @@ -256,12 +256,12 @@ def uninstall_nbextension(dest, require=None, user=False, sys_prefix=False, pref full_dest = pjoin(nbext, dest) if os.path.lexists(full_dest): if logger: - logger.info("Removing: %s" % full_dest) + logger.info(f"Removing: {full_dest}") if os.path.isdir(full_dest) and not os.path.islink(full_dest): shutil.rmtree(full_dest) else: os.remove(full_dest) - + # Look through all of the config sections making sure that the nbextension # doesn't exist. config_dir = os.path.join(_get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig') @@ -281,7 +281,7 @@ def _find_uninstall_nbextension(filename, logger=None): path = pjoin(nbext, filename) if os.path.lexists(path): if logger: - logger.info("Removing: %s" % path) + logger.info(f"Removing: {path}") if os.path.isdir(path) and not os.path.islink(path): shutil.rmtree(path) else: @@ -295,7 +295,7 @@ def uninstall_nbextension_python(module, user=False, sys_prefix=False, prefix=None, nbextensions_dir=None, logger=None): """Uninstall an nbextension bundled in a Python package. - + See parameters of `install_nbextension_python` """ m, nbexts = _get_nbextension_metadata(module) @@ -303,8 +303,8 @@ def uninstall_nbextension_python(module, dest = nbext['dest'] require = nbext['require'] if logger: - logger.info("Uninstalling {} {}".format(dest, require)) - uninstall_nbextension(dest, require, user=user, sys_prefix=sys_prefix, + logger.info(f"Uninstalling {dest} {require}") + uninstall_nbextension(dest, require, user=user, sys_prefix=sys_prefix, prefix=prefix, nbextensions_dir=nbextensions_dir, logger=logger) @@ -335,11 +335,7 @@ def _set_nbextension_state(section, require, state, _get_config_dir(user=user, sys_prefix=sys_prefix), 'nbconfig') cm = BaseJSONConfigManager(config_dir=config_dir) if logger: - logger.info("{} {} extension {}...".format( - "Enabling" if state else "Disabling", - section, - require - )) + logger.info(f"{'Enabling' if state else 'Disabling'} {section} extension {require}...") cm.update(section, {"load_extensions": {require: state}}) validate_nbextension(require, logger=logger) @@ -408,7 +404,7 @@ def enable_nbextension(section, require, user=True, sys_prefix=False, def disable_nbextension(section, require, user=True, sys_prefix=False, logger=None): """Disable a named nbextension - + Returns True if the final state is the one requested. Parameters @@ -478,7 +474,7 @@ def enable_nbextension_python(module, user=True, sys_prefix=False, def disable_nbextension_python(module, user=True, sys_prefix=False, logger=None): """Disable some nbextensions associated with a Python module. - + Returns True if the final state is the one requested. Parameters @@ -516,27 +512,27 @@ def validate_nbextension(require, logger=None): js_exists = False for exts in jupyter_path('nbextensions'): # Does the Javascript entrypoint actually exist on disk? - js = u"{}.js".format(os.path.join(exts, *require.split("/"))) + js = f"{os.path.join(exts, *require.split('/'))}.js" js_exists = os.path.exists(js) if js_exists: break - require_tmpl = u" - require? {} {}" + require_tmpl = " - require? {} {}" if js_exists: infos.append(require_tmpl.format(GREEN_OK, require)) else: warnings.append(require_tmpl.format(RED_X, require)) - + if logger: if warnings: - logger.warning(u" - Validating: problems found:") + logger.warning(" - Validating: problems found:") for msg in warnings: logger.warning(msg) for msg in infos: logger.info(msg) else: - logger.info(u" - Validating: {}".format(GREEN_OK)) - + logger.info(f" - Validating: {GREEN_OK}") + return warnings @@ -567,19 +563,19 @@ def validate_nbextension_python(spec, full_dest, logger=None): section = spec.get("section", None) if section in NBCONFIG_SECTIONS: - infos.append(u" {} section: {}".format(GREEN_OK, section)) + infos.append(f" {GREEN_OK} section: {section}") else: - warnings.append(u" {} section: {}".format(RED_X, section)) + warnings.append(f" {RED_X} section: {section}") require = spec.get("require", None) if require is not None: require_path = os.path.join( full_dest[0:-len(spec["dest"])], - u"{}.js".format(require)) + f"{require}.js") if os.path.exists(require_path): - infos.append(u" {} require: {}".format(GREEN_OK, require_path)) + infos.append(f" {GREEN_OK} require: {require_path}") else: - warnings.append(u" {} require: {}".format(RED_X, require_path)) + warnings.append(f" {RED_X} require: {require_path}") if logger: if warnings: @@ -588,9 +584,9 @@ def validate_nbextension_python(spec, full_dest, logger=None): logger.warning(msg) for msg in infos: logger.info(msg) - logger.warning(u"Full spec: {}".format(spec)) + logger.warning(f"Full spec: {spec}") else: - logger.info(u"- Validating: {}".format(GREEN_OK)) + logger.info(f"- Validating: {GREEN_OK}") return warnings @@ -633,24 +629,24 @@ def validate_nbextension_python(spec, full_dest, logger=None): class InstallNBExtensionApp(BaseExtensionApp): """Entry point for installing notebook extensions""" description = """Install Jupyter notebook extensions - + Usage - + jupyter nbextension install path|url [--user|--sys-prefix] - + This copies a file or a folder into the Jupyter nbextensions directory. If a URL is given, it will be downloaded. If an archive is given, it will be extracted into nbextensions. If the requested files are already up to date, no action is taken unless --overwrite is specified. """ - + examples = """ jupyter nbextension install /path/to/myextension """ aliases = aliases flags = flags - + overwrite = Bool(False, config=True, help="Force overwrite of existing files") symlink = Bool(False, config=True, help="Create symlinks instead of copying files") @@ -662,7 +658,7 @@ class InstallNBExtensionApp(BaseExtensionApp): def _config_file_name_default(self): """The default config file name.""" return 'jupyter_notebook_config' - + def install_extensions(self): """Perform the installation of nbextension(s)""" if len(self.extra_args)>1: @@ -675,7 +671,7 @@ def install_extensions(self): else: install = install_nbextension kwargs = {'destination': self.destination} - + full_dests = install(self.extra_args[0], overwrite=self.overwrite, symlink=self.symlink, @@ -689,14 +685,12 @@ def install_extensions(self): if full_dests: self.log.info( - u"\nTo initialize this nbextension in the browser every time" - " the notebook (or other app) loads:\n\n" - " jupyter nbextension enable {}{}{}{}\n".format( - self.extra_args[0] if self.python else "", - " --user" if self.user else "", - " --py" if self.python else "", - " --sys-prefix" if self.sys_prefix else "" - ) + f"\nTo initialize this nbextension in the browser every time" + f" the notebook (or other app) loads:\n\n" + f" jupyter nbextension enable {self.extra_args[0] if self.python else ''}" + f"{' --user' if self.user else ''}" + f"{' --py' if self.python else ''}" + f"{' --sys-prefix' if self.sys_prefix else ''}\n" ) def start(self): @@ -714,9 +708,9 @@ class UninstallNBExtensionApp(BaseExtensionApp): """Entry point for uninstalling notebook extensions""" version = __version__ description = """Uninstall Jupyter notebook extensions - + Usage - + jupyter nbextension uninstall path/url path/url/entrypoint jupyter nbextension uninstall --py pythonPackageName @@ -734,12 +728,12 @@ class UninstallNBExtensionApp(BaseExtensionApp): It will uninstall nbextensions listed in that module, but not the module itself (which you should uninstall using a package manager such as pip). """ - + examples = """ jupyter nbextension uninstall dest/dir dest/dir/extensionjs jupyter nbextension uninstall --py extensionPyPackage """ - + aliases = { "prefix" : "UninstallNBExtensionApp.prefix", "nbextensions" : "UninstallNBExtensionApp.nbextensions_dir", @@ -748,7 +742,7 @@ class UninstallNBExtensionApp(BaseExtensionApp): flags = BaseExtensionApp.flags.copy() flags['system'] = ({'UninstallNBExtensionApp': {'system': True}}, "Uninstall specifically from systemwide installation directory") - + prefix = Unicode('', config=True, help="Installation prefix. Overrides --user, --sys-prefix and --system" ) @@ -759,7 +753,7 @@ class UninstallNBExtensionApp(BaseExtensionApp): system = Bool(False, config=True, help="Uninstall specifically from systemwide installation directory" ) - + def _config_file_name_default(self): """The default config file name.""" return 'jupyter_notebook_config' @@ -800,7 +794,7 @@ def find_uninstall_extension(self): changed = _find_uninstall_nbextension(name, logger=self.log) if not changed: - print("No installed extension %r found." % name) + print(f"No installed extension {name!r} found.") if self.require: for section in NBCONFIG_SECTIONS: @@ -837,13 +831,13 @@ class ToggleNBExtensionApp(BaseExtensionApp): user = Bool(True, config=True, help="Apply the configuration only for the current user (default)") aliases = {'section': 'ToggleNBExtensionApp.section'} - + _toggle_value = None def _config_file_name_default(self): """The default config file name.""" return 'jupyter_notebook_config' - + def toggle_nbextension_python(self, module): """Toggle some extensions in an importable Python module. @@ -878,7 +872,7 @@ def toggle_nbextension(self, require): return toggle(self.section, require, user=self.user, sys_prefix=self.sys_prefix, logger=self.log) - + def start(self): if not self.extra_args: sys.exit('Please specify an nbextension/package to enable or disable') @@ -895,7 +889,7 @@ class EnableNBExtensionApp(ToggleNBExtensionApp): name = "jupyter nbextension enable" description = """ Enable an nbextension in frontend configuration. - + Usage jupyter nbextension enable [--system|--sys-prefix] """ @@ -907,7 +901,7 @@ class DisableNBExtensionApp(ToggleNBExtensionApp): name = "jupyter nbextension disable" description = """ Disable an nbextension in frontend configuration. - + Usage jupyter nbextension disable [--system|--sys-prefix] """ @@ -919,15 +913,15 @@ class ListNBExtensionsApp(BaseExtensionApp): name = "jupyter nbextension list" version = __version__ description = "List all nbextensions known by the configuration system" - + def list_nbextensions(self): """List all the nbextensions""" config_dirs = [os.path.join(p, 'nbconfig') for p in jupyter_config_path()] - + print("Known nbextensions:") - + for config_dir in config_dirs: - head = u' config dir: {}'.format(config_dir) + head = f' config dir: {config_dir}' head_shown = False cm = BaseJSONConfigManager(parent=self, config_dir=config_dir) @@ -938,15 +932,13 @@ def list_nbextensions(self): # only show heading if there is an nbextension here print(head) head_shown = True - print(u' {} section'.format(section)) - + print(f' {section} section') + for require, enabled in data['load_extensions'].items(): - print(u' {} {}'.format( - require, - GREEN_ENABLED if enabled else RED_DISABLED)) + print(f' {require} {GREEN_ENABLED if enabled else RED_DISABLED}') if enabled: validate_nbextension(require, logger=self.log) - + def start(self): """Perform the App's functions as configured""" self.list_nbextensions() @@ -982,7 +974,7 @@ def start(self): # The above should have called a subcommand and raised NoStart; if we # get here, it didn't, so we should self.log.info a message. subcmds = ", ".join(sorted(self.subcommands)) - sys.exit("Please supply at least one subcommand: %s" % subcmds) + sys.exit(f"Please supply at least one subcommand: {subcmds}") main = NBExtensionApp.launch_instance @@ -1012,10 +1004,10 @@ def _should_copy(src, dest, logger=None): # we add a fudge factor to work around a bug in python 2.x # that was fixed in python 3.x: https://bugs.python.org/issue12904 if logger: - logger.warn("Out of date: %s" % dest) + logger.warn(f"Out of date: {dest}") return True if logger: - logger.info("Up to date: %s" % dest) + logger.info(f"Up to date: {dest}") return False @@ -1034,7 +1026,7 @@ def _maybe_copy(src, dest, logger=None): """ if _should_copy(src, dest, logger=logger): if logger: - logger.info("Copying: %s -> %s" % (src, dest)) + logger.info(f"Copying: {src} -> {dest}") shutil.copy2(src, dest) @@ -1051,7 +1043,7 @@ def _safe_is_tarfile(path): """ try: return tarfile.is_tarfile(path) - except IOError: + except OSError: return False @@ -1076,15 +1068,16 @@ def _get_nbextension_dir(user=False, sys_prefix=False, prefix=None, nbextensions ('nbextensions_dir', nbextensions_dir), ('sys_prefix', sys_prefix), ] - conflicting_set = ['{}={!r}'.format(n, v) for n, v in conflicting if v] + conflicting_set = [f'{n}={v!r}' for n, v in conflicting if v] if len(conflicting_set) > 1: raise ArgumentConflict( - "cannot specify more than one of user, sys_prefix, prefix, or nbextensions_dir, but got: {}" - .format(', '.join(conflicting_set))) + f"cannot specify more than one of user, sys_prefix, prefix, or nbextensions_dir, " + f"but got: {', '.join(conflicting_set)}" + ) if user: - nbext = pjoin(jupyter_data_dir(), u'nbextensions') + nbext = pjoin(jupyter_data_dir(), 'nbextensions') elif sys_prefix: - nbext = pjoin(ENV_JUPYTER_PATH[0], u'nbextensions') + nbext = pjoin(ENV_JUPYTER_PATH[0], 'nbextensions') elif prefix: nbext = pjoin(prefix, 'share', 'jupyter', 'nbextensions') elif nbextensions_dir: @@ -1113,8 +1106,10 @@ def _get_nbextension_metadata(module): """ m = import_item(module) if not hasattr(m, '_jupyter_nbextension_paths'): - raise KeyError('The Python module {} is not a valid nbextension, ' - 'it is missing the `_jupyter_nbextension_paths()` method.'.format(module)) + raise KeyError( + f'The Python module {module} is not a valid nbextension, ' + f'it is missing the `_jupyter_nbextension_paths()` method.' + ) nbexts = m._jupyter_nbextension_paths() return m, nbexts diff --git a/notebook/notebook/handlers.py b/notebook/notebook/handlers.py index 638931d6be..a725810054 100644 --- a/notebook/notebook/handlers.py +++ b/notebook/notebook/handlers.py @@ -46,13 +46,12 @@ def get_frontend_exporters(): # Ensure export_from_notebook is explicitly defined & not inherited if ux_name is not None and ux_name != super_uxname: - display = _('{} ({})'.format(ux_name, - exporter_instance.file_extension)) + display = _(f'{ux_name} ({exporter_instance.file_extension})') frontend_exporters.append(ExporterInfo(name, display)) # Ensure default_exporters are in frontend_exporters if not already # This protects against nbconvert versions lower than 5.5 - names = set(exporter.name.lower() for exporter in frontend_exporters) + names = {exporter.name.lower() for exporter in frontend_exporters} for exporter in default_exporters: if exporter.name not in names: frontend_exporters.append(exporter) @@ -74,11 +73,11 @@ class NotebookHandler(IPythonHandler): @web.authenticated @gen.coroutine def get(self, path): - """get renders the notebook template if a name is given, or + """get renders the notebook template if a name is given, or redirects to the '/files/' handler if the name is not given.""" path = path.strip('/') cm = self.contents_manager - + # will raise 404 on not found try: model = yield maybe_future(cm.get(path, content=False)) @@ -109,6 +108,6 @@ def get(self, path): default_handlers = [ - (r"/notebooks%s" % path_regex, NotebookHandler), + (fr"/notebooks{path_regex}", NotebookHandler), ] diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index e07bfcf507..a274f4bb38 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -14,7 +14,6 @@ import hmac import importlib import inspect -import io import ipaddress import json import logging @@ -115,7 +114,6 @@ url_path_join, urldecode_unix_socket_path, urlencode_unix_socket, - urlencode_unix_socket_path, urljoin, ) from .traittypes import TypeFromClasses @@ -217,13 +215,13 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager, env.install_gettext_translations(nbui, newstyle=False) if dev_mode: - DEV_NOTE_NPM = """It looks like you're running the notebook from source. + DEV_NOTE_NPM = f"""It looks like you're running the notebook from source. If you're working on the Javascript of the notebook, try running - %s + {'npm run build:watch'} in another terminal window to have the system incrementally - watch and build the notebook's JavaScript for you, as you make changes.""" % 'npm run build:watch' + watch and build the notebook's JavaScript for you, as you make changes.""" log.info(DEV_NOTE_NPM) if sys_info['commit_source'] == 'repository': @@ -426,7 +424,7 @@ def _config_file_default(self): def start(self): from .auth.security import set_password set_password(config_file=self.config_file) - self.log.info("Wrote hashed password to %s" % self.config_file) + self.log.info(f"Wrote hashed password to {self.config_file}") def shutdown_server(server_info, timeout=5, log=None): @@ -505,9 +503,9 @@ class NbserverStopApp(JupyterApp): description="Stop currently running notebook server." port = Integer(DEFAULT_NOTEBOOK_PORT, config=True, - help="Port of the server to be killed. Default %s" % DEFAULT_NOTEBOOK_PORT) + help=f"Port of the server to be killed. Default {DEFAULT_NOTEBOOK_PORT}") - sock = Unicode(u'', config=True, + sock = Unicode('', config=True, help="UNIX socket of the server to be killed.") def parse_command_line(self, argv=None): @@ -523,7 +521,7 @@ def shutdown_server(self, server): return shutdown_server(server, log=self.log) def _shutdown_or_exit(self, target_endpoint, server): - print("Shutting down server on %s..." % target_endpoint) + print(f"Shutting down server on {target_endpoint}...") server_stopped = self.shutdown_server(server) if not server_stopped and sys.platform.startswith('win'): # the pid check on Windows appears to be unreliable, so fetch another @@ -533,13 +531,13 @@ def _shutdown_or_exit(self, target_endpoint, server): if server not in servers: server_stopped = True if not server_stopped: - sys.exit("Could not stop server on %s" % target_endpoint) + sys.exit(f"Could not stop server on {target_endpoint}") @staticmethod def _maybe_remove_unix_socket(socket_path): try: os.unlink(socket_path) - except (OSError, IOError): + except OSError: pass def start(self): @@ -563,7 +561,7 @@ def start(self): else: current_endpoint = self.sock or self.port print( - "There is currently no server running on {}".format(current_endpoint), + f"There is currently no server running on {current_endpoint}", file=sys.stderr ) print("Ports/sockets currently in use:", file=sys.stderr) @@ -753,7 +751,7 @@ def _default_log_datefmt(self): @default('log_format') def _default_log_format(self): """override default log format to include time""" - return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s" + return "%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s" ignore_minified_js = Bool(False, config=True, @@ -834,7 +832,7 @@ def _default_ip(self): s = socket.socket() try: s.bind(('localhost', 0)) - except socket.error as e: + except OSError as e: self.log.warning(_("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s"), e) return '127.0.0.1' else: @@ -844,11 +842,11 @@ def _default_ip(self): @validate('ip') def _validate_ip(self, proposal): value = proposal['value'] - if value == u'*': - value = u'' + if value == '*': + value = '' return value - custom_display_url = Unicode(u'', config=True, + custom_display_url = Unicode('', config=True, help=_("""Override URL shown to users. Replace actual URL, including protocol, address, port and base URL, @@ -883,7 +881,7 @@ def port_retries_default(self): return int(os.getenv(self.port_retries_env, self.port_retries_default_value)) - sock = Unicode(u'', config=True, + sock = Unicode('', config=True, help=_("The UNIX socket the notebook server will listen on.") ) @@ -914,15 +912,15 @@ def _validate_sock_mode(self, proposal): return value - certfile = Unicode(u'', config=True, + certfile = Unicode('', config=True, help=_("""The full path to an SSL/TLS certificate file.""") ) - keyfile = Unicode(u'', config=True, + keyfile = Unicode('', config=True, help=_("""The full path to a private key file for usage with SSL/TLS.""") ) - client_ca = Unicode(u'', config=True, + client_ca = Unicode('', config=True, help=_("""The full path to a certificate authority certificate for SSL/TLS client authentication.""") ) @@ -947,7 +945,7 @@ def _default_cookie_secret_file(self): @default('cookie_secret') def _default_cookie_secret(self): if os.path.exists(self.cookie_secret_file): - with io.open(self.cookie_secret_file, 'rb') as f: + with open(self.cookie_secret_file, 'rb') as f: key = f.read() else: key = encodebytes(os.urandom(32)) @@ -960,7 +958,7 @@ def _write_cookie_secret_file(self, secret): """write my secret to my secret_file""" self.log.info(_("Writing notebook server cookie secret to %s"), self.cookie_secret_file) try: - with io.open(self.cookie_secret_file, 'wb') as f: + with open(self.cookie_secret_file, 'wb') as f: f.write(secret) except OSError as e: self.log.error(_("Failed to write cookie secret to %s: %s"), @@ -995,12 +993,12 @@ def _token_default(self): return os.getenv('JUPYTER_TOKEN') if os.getenv('JUPYTER_TOKEN_FILE'): self._token_generated = False - with io.open(os.getenv('JUPYTER_TOKEN_FILE'), "r") as token_file: + with open(os.getenv('JUPYTER_TOKEN_FILE')) as token_file: return token_file.read() if self.password: # no token if password is enabled self._token_generated = False - return u'' + return '' else: self._token_generated = True return binascii.hexlify(os.urandom(24)).decode('ascii') @@ -1051,7 +1049,7 @@ def _default_min_open_files_limit(self): def _token_changed(self, change): self._token_generated = False - password = Unicode(u'', config=True, + password = Unicode('', config=True, help="""Hashed password to use for web authentication. To generate, type in a python/IPython shell: @@ -1166,7 +1164,7 @@ def _default_allow_remote(self): (NotebookApp.browser) configuration option. """) - browser = Unicode(u'', config=True, + browser = Unicode('', config=True, help="""Specify what command to use to invoke a web browser when opening the notebook. If not specified, the default browser will be determined by the `webbrowser` @@ -1252,7 +1250,7 @@ def _update_webapp_settings(self, change): def _update_enable_mathjax(self, change): """set mathjax url to empty if mathjax is disabled""" if not change['new']: - self.mathjax_url = u'' + self.mathjax_url = '' base_url = Unicode('/', config=True, help='''The base URL for the notebook server. @@ -1351,7 +1349,7 @@ def nbextensions_path(self): @default('mathjax_url') def _default_mathjax_url(self): if not self.enable_mathjax: - return u'' + return '' static_url_prefix = self.tornado_settings.get("static_url_prefix", "static") return url_path_join(static_url_prefix, 'components', 'MathJax', 'MathJax.js') @@ -1360,7 +1358,7 @@ def _update_mathjax_url(self, change): new = change['new'] if new and not self.enable_mathjax: # enable_mathjax=False overrides mathjax_url - self.mathjax_url = u'' + self.mathjax_url = '' else: self.log.info(_("Using MathJax: %s"), new) @@ -1701,7 +1699,7 @@ def init_resources(self): if hard < soft: hard = soft self.log.debug( - 'Raising open file limit: soft {}->{}; hard {}->{}'.format(old_soft, soft, old_hard, hard) + f'Raising open file limit: soft {old_soft}->{soft}; hard {old_hard}->{hard}' ) resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard)) @@ -1803,7 +1801,7 @@ def _bind_http_server_unix(self): try: sock = bind_unix_socket(self.sock, mode=int(self.sock_mode.encode(), 8)) self.http_server.add_socket(sock) - except socket.error as e: + except OSError as e: if e.errno == errno.EADDRINUSE: self.log.warning(_('The socket %s is already in use.') % self.sock) return False @@ -1820,7 +1818,7 @@ def _bind_http_server_tcp(self): for port in random_ports(self.port, self.port_retries+1): try: self.http_server.listen(port, self.ip) - except socket.error as e: + except OSError as e: eacces = (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)) if sys.platform == 'cygwin': # Cygwin has a bug that causes EPERM to be returned in this @@ -1886,7 +1884,7 @@ def connection_url(self): return self._tcp_url(ip) def _unix_sock_url(self, token=None): - return '%s%s' % (urlencode_unix_socket(self.sock), self.base_url) + return f'{urlencode_unix_socket(self.sock)}{self.base_url}' def _tcp_url(self, ip, port=None): proto = 'https' if self.certfile else 'http' @@ -2411,7 +2409,7 @@ def list_running_servers(runtime_dir=None): for file_name in os.listdir(runtime_dir): if re.match('nbserver-(.+).json', file_name): - with io.open(os.path.join(runtime_dir, file_name), encoding='utf-8') as f: + with open(os.path.join(runtime_dir, file_name), encoding='utf-8') as f: info = json.load(f) # Simple check whether that process is really still running diff --git a/notebook/prometheus/log_functions.py b/notebook/prometheus/log_functions.py index a67a252ade..f82e0f970e 100644 --- a/notebook/prometheus/log_functions.py +++ b/notebook/prometheus/log_functions.py @@ -19,6 +19,6 @@ def prometheus_log_method(handler): """ HTTP_REQUEST_DURATION_SECONDS.labels( method=handler.request.method, - handler='{}.{}'.format(handler.__class__.__module__, type(handler).__name__), + handler=f'{handler.__class__.__module__}.{type(handler).__name__}', status_code=handler.get_status() ).observe(handler.request.request_time()) diff --git a/notebook/serverextensions.py b/notebook/serverextensions.py index 72020a24f5..7a56a8fca8 100644 --- a/notebook/serverextensions.py +++ b/notebook/serverextensions.py @@ -59,14 +59,14 @@ def toggle_serverextension_python(import_name, enabled=None, parent=None, if logger: if new_enabled: - logger.info(u"Enabling: %s" % (import_name)) + logger.info(f"Enabling: {import_name}") else: - logger.info(u"Disabling: %s" % (import_name)) + logger.info(f"Disabling: {import_name}") server_extensions[import_name] = new_enabled if logger: - logger.info(u"- Writing config: {}".format(config_dir)) + logger.info(f"- Writing config: {config_dir}") cm.update("jupyter_notebook_config", cfg) @@ -104,13 +104,13 @@ def validate_serverextension(import_name, logger=None): except Exception: logger.warning("Error loading server extension %s", import_name) - import_msg = u" {} is {} importable?" + import_msg = " {} is {} importable?" if func is not None: infos.append(import_msg.format(GREEN_OK, import_name)) else: warnings.append(import_msg.format(RED_X, import_name)) - post_mortem = u" {} {} {}" + post_mortem = " {} {} {}" if logger: if warnings: [logger.info(info) for info in infos] @@ -159,13 +159,13 @@ class ToggleServerExtensionApp(BaseExtensionApp): """A base class for enabling/disabling extensions""" name = "jupyter serverextension enable/disable" description = "Enable/disable a server extension using frontend configuration files." - + flags = flags user = Bool(True, config=True, help="Whether to do a user install") sys_prefix = Bool(False, config=True, help="Use the sys.prefix as the prefix") python = Bool(False, config=True, help="Install from a Python package") - + def toggle_server_extension(self, import_name): """Change the status of a named server extension. @@ -215,7 +215,7 @@ class EnableServerExtensionApp(ToggleServerExtensionApp): name = "jupyter serverextension enable" description = """ Enable a serverextension in configuration. - + Usage jupyter serverextension enable [--system|--sys-prefix] """ @@ -227,7 +227,7 @@ class DisableServerExtensionApp(ToggleServerExtensionApp): name = "jupyter serverextension disable" description = """ Disable a serverextension in configuration. - + Usage jupyter serverextension disable [--system|--sys-prefix] """ @@ -254,11 +254,9 @@ def list_server_extensions(self): .setdefault("nbserver_extensions", {}) ) if server_extensions: - print(u'config dir: {}'.format(config_dir)) + print(f'config dir: {config_dir}') for import_name, enabled in server_extensions.items(): - print(u' {} {}'.format( - import_name, - GREEN_ENABLED if enabled else RED_DISABLED)) + print(f' {import_name} {GREEN_ENABLED if enabled else RED_DISABLED}') validate_serverextension(import_name, self.log) def start(self): @@ -293,7 +291,7 @@ def start(self): # The above should have called a subcommand and raised NoStart; if we # get here, it didn't, so we should self.log.info a message. subcmds = ", ".join(sorted(self.subcommands)) - sys.exit("Please supply at least one subcommand: %s" % subcmds) + sys.exit(f"Please supply at least one subcommand: {subcmds}") main = ServerExtensionApp.launch_instance @@ -324,7 +322,7 @@ def _get_server_extension_metadata(module): """ m = import_item(module) if not hasattr(m, '_jupyter_server_extension_paths'): - raise KeyError(u'The Python module {} does not include any valid server extensions'.format(module)) + raise KeyError(f'The Python module {module} does not include any valid server extensions') return m, m._jupyter_server_extension_paths() if __name__ == '__main__': diff --git a/notebook/services/config/handlers.py b/notebook/services/config/handlers.py index aae6480757..899eb4cf22 100644 --- a/notebook/services/config/handlers.py +++ b/notebook/services/config/handlers.py @@ -3,9 +3,6 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import json -import os -import io -import errno from tornado import web from ...base.handlers import APIHandler @@ -35,5 +32,5 @@ def patch(self, section_name): section_name_regex = r"(?P\w+)" default_handlers = [ - (r"/api/config/%s" % section_name_regex, ConfigHandler), + (fr"/api/config/{section_name_regex}", ConfigHandler), ] diff --git a/notebook/services/config/tests/test_config_api.py b/notebook/services/config/tests/test_config_api.py index a4df8f40fc..151f0ea6cc 100644 --- a/notebook/services/config/tests/test_config_api.py +++ b/notebook/services/config/tests/test_config_api.py @@ -2,13 +2,11 @@ import json -import requests - from notebook.utils import url_path_join from notebook.tests.launchnotebook import NotebookTestBase -class ConfigAPI(object): +class ConfigAPI: """Wrapper for notebook API calls.""" def __init__(self, request): self.request = request diff --git a/notebook/services/contents/checkpoints.py b/notebook/services/contents/checkpoints.py index c29a669c22..fd80ef8c0e 100644 --- a/notebook/services/contents/checkpoints.py +++ b/notebook/services/contents/checkpoints.py @@ -53,7 +53,7 @@ def delete_all_checkpoints(self, path): self.delete_checkpoint(checkpoint['id'], path) -class GenericCheckpointsMixin(object): +class GenericCheckpointsMixin: """ Helper for creating Checkpoints subclasses that can be used with any ContentsManager. @@ -90,7 +90,7 @@ def create_checkpoint(self, contents_mgr, path): path, ) else: - raise HTTPError(500, u'Unexpected type %s' % type) + raise HTTPError(500, f'Unexpected type {type}') def restore_checkpoint(self, contents_mgr, checkpoint_id, path): """Restore a checkpoint.""" @@ -100,7 +100,7 @@ def restore_checkpoint(self, contents_mgr, checkpoint_id, path): elif type == 'file': model = self.get_file_checkpoint(checkpoint_id, path) else: - raise HTTPError(500, u'Unexpected type %s' % type) + raise HTTPError(500, f'Unexpected type {type}') contents_mgr.save(model, path) # Required Methods diff --git a/notebook/services/contents/filecheckpoints.py b/notebook/services/contents/filecheckpoints.py index 5a9c835749..540091fd50 100644 --- a/notebook/services/contents/filecheckpoints.py +++ b/notebook/services/contents/filecheckpoints.py @@ -50,7 +50,7 @@ def _root_dir_default(self): # ContentsManager-dependent checkpoint API def create_checkpoint(self, contents_mgr, path): """Create a checkpoint.""" - checkpoint_id = u'checkpoint' + checkpoint_id = 'checkpoint' src_path = contents_mgr._get_os_path(path) dest_path = self.checkpoint_path(checkpoint_id, path) self._copy(src_path, dest_path) @@ -107,11 +107,7 @@ def checkpoint_path(self, checkpoint_id, path): parent, name = ('/' + path).rsplit('/', 1) parent = parent.strip('/') basename, ext = os.path.splitext(name) - filename = u"{name}-{checkpoint_id}{ext}".format( - name=basename, - checkpoint_id=checkpoint_id, - ext=ext, - ) + filename = f"{basename}-{checkpoint_id}{ext}" os_path = self._get_os_path(path=parent) cp_dir = os.path.join(os_path, self.checkpoint_dir) with self.perm_to_403(): @@ -133,7 +129,7 @@ def checkpoint_model(self, checkpoint_id, os_path): def no_such_checkpoint(self, path, checkpoint_id): raise HTTPError( 404, - u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id) + f'Checkpoint does not exist: {path}@{checkpoint_id}' ) @@ -146,7 +142,7 @@ def create_file_checkpoint(self, content, format, path): """Create a checkpoint from the current content of a file.""" path = path.strip('/') # only the one checkpoint ID: - checkpoint_id = u"checkpoint" + checkpoint_id = "checkpoint" os_checkpoint_path = self.checkpoint_path(checkpoint_id, path) self.log.debug("creating checkpoint for %s", path) with self.perm_to_403(): @@ -159,7 +155,7 @@ def create_notebook_checkpoint(self, nb, path): """Create a checkpoint from the current content of a notebook.""" path = path.strip('/') # only the one checkpoint ID: - checkpoint_id = u"checkpoint" + checkpoint_id = "checkpoint" os_checkpoint_path = self.checkpoint_path(checkpoint_id, path) self.log.debug("creating checkpoint for %s", path) with self.perm_to_403(): diff --git a/notebook/services/contents/fileio.py b/notebook/services/contents/fileio.py index f99504e32b..79482cf9ca 100644 --- a/notebook/services/contents/fileio.py +++ b/notebook/services/contents/fileio.py @@ -7,7 +7,6 @@ from contextlib import contextmanager import errno -import io import os import shutil @@ -102,9 +101,9 @@ def atomic_writing(path, text=True, encoding='utf-8', log=None, **kwargs): if text: # Make sure that text files have Unix linefeeds by default kwargs.setdefault('newline', '\n') - fileobj = io.open(path, 'w', encoding=encoding, **kwargs) + fileobj = open(path, 'w', encoding=encoding, **kwargs) else: - fileobj = io.open(path, 'wb', **kwargs) + fileobj = open(path, 'wb', **kwargs) try: yield fileobj @@ -154,9 +153,9 @@ def _simple_writing(path, text=True, encoding='utf-8', log=None, **kwargs): if text: # Make sure that text files have Unix linefeeds by default kwargs.setdefault('newline', '\n') - fileobj = io.open(path, 'w', encoding=encoding, **kwargs) + fileobj = open(path, 'w', encoding=encoding, **kwargs) else: - fileobj = io.open(path, 'wb', **kwargs) + fileobj = open(path, 'wb', **kwargs) try: yield fileobj @@ -197,7 +196,7 @@ class FileManagerMixin(Configurable): def open(self, os_path, *args, **kwargs): """wrapper around io.open that turns permission errors into 403""" with self.perm_to_403(os_path): - with io.open(os_path, *args, **kwargs) as f: + with open(os_path, *args, **kwargs) as f: yield f @contextmanager @@ -218,7 +217,7 @@ def perm_to_403(self, os_path=''): """context manager for turning permission errors into 403.""" try: yield - except (OSError, IOError) as e: + except OSError as e: if e.errno in {errno.EPERM, errno.EACCES}: # make 403 error message without root prefix # this may not work perfectly on unicode paths on Python 2, @@ -226,7 +225,7 @@ def perm_to_403(self, os_path=''): if not os_path: os_path = str_to_unicode(e.filename or 'unknown file') path = to_api_path(os_path, root=self.root_dir) - raise HTTPError(403, u'Permission denied: %s' % path) from e + raise HTTPError(403, f'Permission denied: {path}') from e else: raise @@ -257,7 +256,7 @@ def _get_os_path(self, path): root = os.path.abspath(self.root_dir) os_path = to_os_path(path, root) if not (os.path.abspath(os_path) + os.path.sep).startswith(root): - raise HTTPError(404, "%s is outside root contents directory" % path) + raise HTTPError(404, f"{path} is outside root contents directory") return os_path def _read_notebook(self, os_path, as_version=4): @@ -276,7 +275,7 @@ def _read_notebook(self, os_path, as_version=4): if not self.use_atomic_writing or not os.path.exists(tmp_path): raise HTTPError( 400, - u"Unreadable Notebook: %s %r" % (os_path, e_orig), + f"Unreadable Notebook: {os_path} {e_orig!r}", ) # Move the bad file aside, restore the intermediate, and try again. @@ -300,7 +299,7 @@ def _read_file(self, os_path, format): If not specified, try to decode as UTF-8, and fall back to base64 """ if not os.path.isfile(os_path): - raise HTTPError(400, "Cannot read non-file %s" % os_path) + raise HTTPError(400, f"Cannot read non-file {os_path}") with self.open(os_path, 'rb') as f: bcontent = f.read() @@ -314,7 +313,7 @@ def _read_file(self, os_path, format): if format == 'text': raise HTTPError( 400, - "%s is not UTF-8 encoded" % os_path, + f"{os_path} is not UTF-8 encoded", reason='bad format', ) from e return encodebytes(bcontent).decode('ascii'), 'base64' @@ -334,7 +333,7 @@ def _save_file(self, os_path, content, format): bcontent = decodebytes(b64_bytes) except Exception as e: raise HTTPError( - 400, u'Encoding error saving %s: %s' % (os_path, e) + 400, f'Encoding error saving {os_path}: {e}' ) from e with self.atomic_writing(os_path, text=False) as f: diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index 0c9386b2fc..b517ca4702 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -5,7 +5,6 @@ from datetime import datetime import errno -import io import os import shutil import stat @@ -60,7 +59,7 @@ def _post_save_script(model, os_path, contents_manager, **kwargs): script, resources = _script_exporter.from_filename(os_path) script_fname = base + resources.get('output_extension', '.txt') log.info("Saving script /%s", to_api_path(script_fname, contents_manager.root_dir)) - with io.open(script_fname, 'w', encoding='utf-8') as f: + with open(script_fname, 'w', encoding='utf-8') as f: f.write(script) @@ -132,8 +131,7 @@ def run_post_save_hook(self, model, os_path): self.post_save_hook(os_path=os_path, model=model, contents_manager=self) except Exception as e: self.log.error("Post-save hook failed o-n %s", os_path, exc_info=True) - raise web.HTTPError(500, u'Unexpected error while running post hook save: %s' - % e) from e + raise web.HTTPError(500, f'Unexpected error while running post hook save: {e}') from e @validate('root_dir') def _validate_root_dir(self, proposal): @@ -143,7 +141,7 @@ def _validate_root_dir(self, proposal): # If we receive a non-absolute path, make it absolute. value = os.path.abspath(value) if not os.path.isdir(value): - raise TraitError("%r is not a directory" % value) + raise TraitError(f"{value!r} is not a directory") return value @default('checkpoints_class') @@ -243,14 +241,14 @@ def _base_model(self, path): """Build the common base of a contents model""" os_path = self._get_os_path(path) info = os.lstat(os_path) - + try: - # size of file + # size of file size = info.st_size except (ValueError, OSError): self.log.warning('Unable to get size.') size = None - + try: last_modified = tz.utcfromtimestamp(info.st_mtime) except (ValueError, OSError): @@ -292,7 +290,7 @@ def _dir_model(self, path, content=True): """ os_path = self._get_os_path(path) - four_o_four = u'directory does not exist: %r' % path + four_o_four = f'directory does not exist: {path!r}' if not os.path.isdir(os_path): raise web.HTTPError(404, four_o_four) @@ -336,7 +334,7 @@ def _dir_model(self, path, content=True): if self.should_list(name): if self.allow_hidden or not is_file_hidden(os_path, stat_res=st): contents.append( - self.get(path='%s/%s' % (path, name), content=False) + self.get(path=f'{path}/{name}', content=False) ) except OSError as e: # ELOOP: recursive symlink @@ -392,14 +390,14 @@ def _notebook_model(self, path, content=True): model = self._base_model(path) model['type'] = 'notebook' os_path = self._get_os_path(path) - + if content: nb = self._read_notebook(os_path, as_version=4) self.mark_trusted_cells(nb, path) model['content'] = nb model['format'] = 'json' self.validate_notebook_model(model) - + return model def get(self, path, content=True, type=None, format=None): @@ -427,32 +425,33 @@ def get(self, path, content=True, type=None, format=None): path = path.strip('/') if not self.exists(path): - raise web.HTTPError(404, u'No such file or directory: %s' % path) + raise web.HTTPError(404, f'No such file or directory: {path}') os_path = self._get_os_path(path) if os.path.isdir(os_path): if type not in (None, 'directory'): raise web.HTTPError(400, - u'%s is a directory, not a %s' % (path, type), reason='bad type') + f'{path} is a directory, not a {type}', reason='bad type') model = self._dir_model(path, content=content) elif type == 'notebook' or (type is None and path.endswith('.ipynb')): model = self._notebook_model(path, content=content) else: if type == 'directory': - raise web.HTTPError(400, - u'%s is not a directory' % path, reason='bad type') + raise web.HTTPError( + 400, + f'{path} is not a directory', reason='bad type') model = self._file_model(path, content=content, format=format) return model def _save_directory(self, os_path, model, path=''): """create a directory""" if is_hidden(os_path, self.root_dir) and not self.allow_hidden: - raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path) + raise web.HTTPError(400, f'Cannot create hidden directory {os_path!r}') if not os.path.exists(os_path): with self.perm_to_403(): os.mkdir(os_path) elif not os.path.isdir(os_path): - raise web.HTTPError(400, u'Not a directory: %s' % (os_path)) + raise web.HTTPError(400, f'Not a directory: {os_path}') else: self.log.debug("Directory %r already exists", os_path) @@ -461,9 +460,9 @@ def save(self, model, path=''): path = path.strip('/') if 'type' not in model: - raise web.HTTPError(400, u'No file type provided') + raise web.HTTPError(400, 'No file type provided') if 'content' not in model and model['type'] != 'directory': - raise web.HTTPError(400, u'No file content provided') + raise web.HTTPError(400, 'No file content provided') os_path = self._get_os_path(path) self.log.debug("Saving %s", os_path) @@ -484,13 +483,12 @@ def save(self, model, path=''): elif model['type'] == 'directory': self._save_directory(os_path, model, path) else: - raise web.HTTPError(400, "Unhandled contents type: %s" % model['type']) + raise web.HTTPError(400, f"Unhandled contents type: {model['type']}") except web.HTTPError: raise except Exception as e: - self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True) - raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % - (path, e)) from e + self.log.error('Error while saving file: %s %s', path, e, exc_info=True) + raise web.HTTPError(500, f'Unexpected error while saving file: {path} {e}') from e validation_message = None if model['type'] == 'notebook': @@ -511,7 +509,7 @@ def delete_file(self, path): os_path = self._get_os_path(path) rm = os.unlink if not os.path.exists(os_path): - raise web.HTTPError(404, u'File or directory does not exist: %s' % os_path) + raise web.HTTPError(404, f'File or directory does not exist: {os_path}') def is_non_empty_dir(os_path): if os.path.isdir(os_path): @@ -527,7 +525,7 @@ def is_non_empty_dir(os_path): if sys.platform == 'win32' and is_non_empty_dir(os_path): # send2trash can really delete files on Windows, so disallow # deleting non-empty files. See Github issue 3631. - raise web.HTTPError(400, u'Directory %s not empty' % os_path) + raise web.HTTPError(400, f'Directory {os_path} not empty') try: self.log.debug("Sending %s to trash", os_path) send2trash(os_path) @@ -538,7 +536,7 @@ def is_non_empty_dir(os_path): if os.path.isdir(os_path): # Don't permanently delete non-empty directories. if is_non_empty_dir(os_path): - raise web.HTTPError(400, u'Directory %s not empty' % os_path) + raise web.HTTPError(400, f'Directory {os_path} not empty') self.log.debug("Removing directory %s", os_path) with self.perm_to_403(): shutil.rmtree(os_path) @@ -563,7 +561,7 @@ def rename_file(self, old_path, new_path): # Should we proceed with the move? if os.path.exists(new_os_path) and not samefile(old_os_path, new_os_path): - raise web.HTTPError(409, u'File already exists: %s' % new_path) + raise web.HTTPError(409, f'File already exists: {new_path}') # Move the file try: @@ -572,8 +570,7 @@ def rename_file(self, old_path, new_path): except web.HTTPError: raise except Exception as e: - raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % - (old_path, e)) from e + raise web.HTTPError(500, f'Unknown error renaming file: {old_path} {e}') from e def info_string(self): return _("Serving notebooks from local directory: %s") % self.root_dir @@ -604,6 +601,6 @@ def _validate_path(path): for char in invalid_chars: if char in path: - raise web.HTTPError(400, "Path '{}' contains characters that are invalid for the filesystem. " - "Path names on this filesystem cannot contain any of the following " - "characters: {}".format(path, invalid_chars)) + raise web.HTTPError(400, f"Path '{path}' contains characters that are invalid for the filesystem. " + f"Path names on this filesystem cannot contain any of the following " + f"characters: {invalid_chars}") diff --git a/notebook/services/contents/handlers.py b/notebook/services/contents/handlers.py index b3216335bb..1a8f2a2aab 100644 --- a/notebook/services/contents/handlers.py +++ b/notebook/services/contents/handlers.py @@ -45,7 +45,7 @@ def validate_model(model, expect_content): if missing: raise web.HTTPError( 500, - u"Missing Model Keys: {missing}".format(missing=missing), + f"Missing Model Keys: {missing}", ) maybe_none_keys = ['content', 'format'] @@ -54,7 +54,7 @@ def validate_model(model, expect_content): if errors: raise web.HTTPError( 500, - u"Keys unexpectedly None: {keys}".format(keys=errors), + f"Keys unexpectedly None: {errors}", ) else: errors = { @@ -65,7 +65,7 @@ def validate_model(model, expect_content): if errors: raise web.HTTPError( 500, - u"Keys unexpectedly not None: {keys}".format(keys=errors), + f"Keys unexpectedly not None: {errors}", ) @@ -103,14 +103,14 @@ def get(self, path=''): path = path or '' type = self.get_query_argument('type', default=None) if type not in {None, 'directory', 'file', 'notebook'}: - raise web.HTTPError(400, u'Type %r is invalid' % type) + raise web.HTTPError(400, f'Type {type!r} is invalid') format = self.get_query_argument('format', default=None) if format not in {None, 'text', 'base64'}: - raise web.HTTPError(400, u'Format %r is invalid' % format) + raise web.HTTPError(400, f'Format {format!r} is invalid') content = self.get_query_argument('content', default='1') if content not in {'0', '1'}: - raise web.HTTPError(400, u'Content %r is invalid' % content) + raise web.HTTPError(400, f'Content {content!r} is invalid') content = int(content) model = yield maybe_future(self.contents_manager.get( @@ -126,7 +126,7 @@ def patch(self, path=''): cm = self.contents_manager model = self.get_json_body() if model is None: - raise web.HTTPError(400, u'JSON body missing') + raise web.HTTPError(400, 'JSON body missing') model = yield maybe_future(cm.update(model, path)) validate_model(model, expect_content=False) self._finish_model(model) @@ -134,10 +134,7 @@ def patch(self, path=''): @gen.coroutine def _copy(self, copy_from, copy_to=None): """Copy a file, optionally specifying a target directory.""" - self.log.info(u"Copying {copy_from} to {copy_to}".format( - copy_from=copy_from, - copy_to=copy_to or '', - )) + self.log.info(f"Copying {copy_from} to {copy_to or ''}") model = yield maybe_future(self.contents_manager.copy(copy_from, copy_to)) self.set_status(201) validate_model(model, expect_content=False) @@ -146,7 +143,7 @@ def _copy(self, copy_from, copy_to=None): @gen.coroutine def _upload(self, model, path): """Handle upload of a new file to path""" - self.log.info(u"Uploading file to %s", path) + self.log.info("Uploading file to %s", path) model = yield maybe_future(self.contents_manager.new(model, path)) self.set_status(201) validate_model(model, expect_content=False) @@ -155,7 +152,7 @@ def _upload(self, model, path): @gen.coroutine def _new_untitled(self, path, type='', ext=''): """Create a new, empty untitled entity""" - self.log.info(u"Creating new %s in %s", type or 'file', path) + self.log.info("Creating new %s in %s", type or 'file', path) model = yield maybe_future(self.contents_manager.new_untitled(path=path, type=type, ext=ext)) self.set_status(201) validate_model(model, expect_content=False) @@ -166,7 +163,7 @@ def _save(self, model, path): """Save an existing file.""" chunk = model.get("chunk", None) if not chunk or chunk == -1: # Avoid tedious log information - self.log.info(u"Saving file at %s", path) + self.log.info("Saving file at %s", path) model = yield maybe_future(self.contents_manager.save(model, path)) validate_model(model, expect_content=False) self._finish_model(model) @@ -193,7 +190,7 @@ def post(self, path=''): dir_exists = yield maybe_future(cm.dir_exists(path)) if not dir_exists: - raise web.HTTPError(404, "No such directory: %s" % path) + raise web.HTTPError(404, f"No such directory: {path}") model = self.get_json_body() @@ -323,10 +320,10 @@ def post(self,path=''): _checkpoint_id_regex = r"(?P[\w-]+)" default_handlers = [ - (r"/api/contents%s/checkpoints" % path_regex, CheckpointsHandler), - (r"/api/contents%s/checkpoints/%s" % (path_regex, _checkpoint_id_regex), + (fr"/api/contents{path_regex}/checkpoints", CheckpointsHandler), + (fr"/api/contents{path_regex}/checkpoints/{_checkpoint_id_regex}", ModifyCheckpointsHandler), - (r"/api/contents%s/trust" % path_regex, TrustNotebooksHandler), - (r"/api/contents%s" % path_regex, ContentsHandler), + (fr"/api/contents{path_regex}/trust", TrustNotebooksHandler), + (fr"/api/contents{path_regex}", ContentsHandler), (r"/api/notebooks/?(.*)", NotebooksRedirectHandler), ] diff --git a/notebook/services/contents/largefilemanager.py b/notebook/services/contents/largefilemanager.py index 6779a0b5c2..38aeeef290 100644 --- a/notebook/services/contents/largefilemanager.py +++ b/notebook/services/contents/largefilemanager.py @@ -1,9 +1,7 @@ from notebook.services.contents.filemanager import FileContentsManager -from contextlib import contextmanager from tornado import web -import nbformat import base64 -import os, io +import os class LargeFileManager(FileContentsManager): """Handle large file upload.""" @@ -13,13 +11,13 @@ def save(self, model, path=''): chunk = model.get('chunk', None) if chunk is not None: path = path.strip('/') - + if 'type' not in model: - raise web.HTTPError(400, u'No file type provided') + raise web.HTTPError(400, 'No file type provided') if model['type'] != 'file': - raise web.HTTPError(400, u'File type "{}" is not supported for large file transfer'.format(model['type'])) + raise web.HTTPError(400, f'File type "{model["type"]}" is not supported for large file transfer') if 'content' not in model and model['type'] != 'directory': - raise web.HTTPError(400, u'No file content provided') + raise web.HTTPError(400, 'No file content provided') os_path = self._get_os_path(path) @@ -33,8 +31,8 @@ def save(self, model, path=''): except web.HTTPError: raise except Exception as e: - self.log.error(u'Error while saving file: %s %s', path, e, exc_info=True) - raise web.HTTPError(500, u'Unexpected error while saving file: %s %s' % (path, e)) from e + self.log.error('Error while saving file: %s %s', path, e, exc_info=True) + raise web.HTTPError(500, f'Unexpected error while saving file: {path} {e}') from e model = self.get(path, content=False) @@ -60,11 +58,11 @@ def _save_large_file(self, os_path, content, format): bcontent = base64.b64decode(b64_bytes) except Exception as e: raise web.HTTPError( - 400, u'Encoding error saving %s: %s' % (os_path, e) + 400, f'Encoding error saving {os_path}: {e}' ) from e with self.perm_to_403(os_path): if os.path.islink(os_path): os_path = os.path.join(os.path.dirname(os_path), os.readlink(os_path)) - with io.open(os_path, 'ab') as f: + with open(os_path, 'ab') as f: f.write(bcontent) diff --git a/notebook/services/contents/manager.py b/notebook/services/contents/manager.py index b556abc1d3..dd8f848c7e 100644 --- a/notebook/services/contents/manager.py +++ b/notebook/services/contents/manager.py @@ -55,7 +55,7 @@ class ContentsManager(LoggingConfigurable): indicating the root path. """ - + root_dir = Unicode('/', config=True) allow_hidden = Bool(False, config=True, help="Allow access to hidden files") @@ -65,7 +65,7 @@ def _notary_default(self): return sign.NotebookNotary(parent=self) hide_globs = List(Unicode(), [ - u'__pycache__', '*.pyc', '*.pyo', + '__pycache__', '*.pyc', '*.pyo', '.DS_Store', '*.so', '*.dylib', '*~', ], config=True, help=""" Glob patterns to hide in file and directory listings. @@ -302,7 +302,7 @@ def info_string(self): def get_kernel_path(self, path, model=None): """Return the API path for the kernel - + KernelManagers can turn this value into a filesystem path, or ignore it altogether. @@ -334,17 +334,16 @@ def increment_filename(self, filename, path='', insert=''): basename, dot, ext = filename.rpartition('.') if ext != 'ipynb': basename, dot, ext = filename.partition('.') - + suffix = dot + ext for i in itertools.count(): if i: - insert_i = '{}{}'.format(insert, i) + insert_i = f'{insert}{i}' else: insert_i = '' - name = u'{basename}{insert}{suffix}'.format(basename=basename, - insert=insert_i, suffix=suffix) - if not self.exists(u'{}/{}'.format(path, name)): + name = f'{basename}{insert_i}{suffix}' + if not self.exists(f'{path}/{name}'): break return name @@ -353,33 +352,32 @@ def validate_notebook_model(self, model): try: validate_nb(model['content']) except ValidationError as e: - model['message'] = u'Notebook validation failed: {}:\n{}'.format( - e.message, json.dumps(e.instance, indent=1, default=lambda obj: ''), - ) + model['message'] = f'Notebook validation failed: {e.message}:\n' \ + f'{json.dumps(e.instance, indent=1, default=lambda obj: "")}' return model - + def new_untitled(self, path='', type='', ext=''): """Create a new untitled file or directory in path - + path must be a directory - + File extension can be specified. - + Use `new` to create files with a fully specified path (including filename). """ path = path.strip('/') if not self.dir_exists(path): - raise HTTPError(404, 'No such directory: %s' % path) - + raise HTTPError(404, f'No such directory: {path}') + model = {} if type: model['type'] = type - + if ext == '.ipynb': model.setdefault('type', 'notebook') else: model.setdefault('type', 'file') - + insert = '' if model['type'] == 'directory': untitled = self.untitled_directory @@ -390,26 +388,26 @@ def new_untitled(self, path='', type='', ext=''): elif model['type'] == 'file': untitled = self.untitled_file else: - raise HTTPError(400, "Unexpected model type: %r" % model['type']) - + raise HTTPError(400, f"Unexpected model type: {model['type']!r}") + name = self.increment_filename(untitled + ext, path, insert=insert) - path = u'{0}/{1}'.format(path, name) + path = f'{path}/{name}' return self.new(model, path) - + def new(self, model=None, path=''): """Create a new file or directory and return its model with no content. - + To create a new untitled entity in a directory, use `new_untitled`. """ path = path.strip('/') if model is None: model = {} - + if path.endswith('.ipynb'): model.setdefault('type', 'notebook') else: model.setdefault('type', 'file') - + # no content, not a directory, so fill out new-file model if 'content' not in model and model['type'] != 'directory': if model['type'] == 'notebook': @@ -419,7 +417,7 @@ def new(self, model=None, path=''): model['content'] = '' model['type'] = 'file' model['format'] = 'text' - + model = self.save(model, path) return model @@ -429,7 +427,7 @@ def copy(self, from_path, to_path=None): If to_path not specified, it will be the parent directory of from_path. If to_path is a directory, filename will increment `from_path-Copy#.ext`. Considering multi-part extensions, the Copy# part will be placed before the first dot for all the extensions except `ipynb`. - For easier manual searching in case of notebooks, the Copy# part will be placed before the last dot. + For easier manual searching in case of notebooks, the Copy# part will be placed before the last dot. from_path must be a full path to a file. """ @@ -442,20 +440,20 @@ def copy(self, from_path, to_path=None): else: from_dir = '' from_name = path - + model = self.get(path) model.pop('path', None) model.pop('name', None) if model['type'] == 'directory': raise HTTPError(400, "Can't copy directories") - + if to_path is None: to_path = from_dir if self.dir_exists(to_path): - name = copy_pat.sub(u'.', from_name) + name = copy_pat.sub('.', from_name) to_name = self.increment_filename(name, to_path, insert='-Copy') - to_path = u'{0}/{1}'.format(to_path, to_name) - + to_path = f'{to_path}/{to_name}' + model = self.save(model, to_path) return model diff --git a/notebook/services/contents/tests/test_contents_api.py b/notebook/services/contents/tests/test_contents_api.py index 6e4ad49dbc..592d22365b 100644 --- a/notebook/services/contents/tests/test_contents_api.py +++ b/notebook/services/contents/tests/test_contents_api.py @@ -2,7 +2,6 @@ from contextlib import contextmanager from functools import partial -import io import json import os import shutil @@ -28,10 +27,7 @@ from ipython_genutils import py3compat from ipython_genutils.tempdir import TemporaryDirectory -try: #PY3 - from base64 import encodebytes, decodebytes -except ImportError: #PY2 - from base64 import encodestring as encodebytes, decodestring as decodebytes +from base64 import encodebytes, decodebytes def uniq_stable(elems): @@ -50,7 +46,7 @@ def dirs_only(dir_model): return [x for x in dir_model['content'] if x['type']=='directory'] -class API(object): +class API: """Wrapper for contents API calls.""" def __init__(self, request): self.request = request @@ -128,16 +124,16 @@ class APITest(NotebookTestBase): """Test the kernels web service API""" dirs_nbs = [('', 'inroot'), ('Directory with spaces in', 'inspace'), - (u'unicodé', 'innonascii'), + ('unicodé', 'innonascii'), ('foo', 'a'), ('foo', 'b'), ('foo', 'name with spaces'), - ('foo', u'unicodé'), + ('foo', 'unicodé'), ('foo/bar', 'baz'), ('ordering', 'A'), ('ordering', 'b'), ('ordering', 'C'), - (u'å b', u'ç d'), + ('å b', 'ç d'), ] hidden_dirs = ['.hidden', '__pycache__'] @@ -151,36 +147,36 @@ def _blob_for_name(name): @staticmethod def _txt_for_name(name): - return u'%s text file' % name - + return f'{name} text file' + def to_os_path(self, api_path): return to_os_path(api_path, root=self.notebook_dir) - + def make_dir(self, api_path): """Create a directory at api_path""" os_path = self.to_os_path(api_path) try: os.makedirs(os_path) except OSError: - print("Directory already exists: %r" % os_path) + print(f"Directory already exists: {os_path!r}") def make_txt(self, api_path, txt): """Make a text file at a given api_path""" os_path = self.to_os_path(api_path) - with io.open(os_path, 'w', encoding='utf-8') as f: + with open(os_path, 'w', encoding='utf-8') as f: f.write(txt) - + def make_blob(self, api_path, blob): """Make a binary file at a given api_path""" os_path = self.to_os_path(api_path) - with io.open(os_path, 'wb') as f: + with open(os_path, 'wb') as f: f.write(blob) - + def make_nb(self, api_path, nb): """Make a notebook file at a given api_path""" os_path = self.to_os_path(api_path) - - with io.open(os_path, 'w', encoding='utf-8') as f: + + with open(os_path, 'w', encoding='utf-8') as f: write(nb, f, version=4) def delete_dir(self, api_path): @@ -192,10 +188,10 @@ def delete_file(self, api_path): """Delete a file at the given path if it exists.""" if self.isfile(api_path): os.unlink(self.to_os_path(api_path)) - + def isfile(self, api_path): return os.path.isfile(self.to_os_path(api_path)) - + def isdir(self, api_path): return os.path.isdir(self.to_os_path(api_path)) @@ -215,18 +211,18 @@ def setUp(self): for d, name in self.dirs_nbs: # create a notebook nb = new_notebook() - nbname = u'{}/{}.ipynb'.format(d, name) + nbname = f'{d}/{name}.ipynb' self.make_nb(nbname, nb) self.addCleanup(partial(self.delete_file, nbname)) # create a text file txt = self._txt_for_name(name) - txtname = u'{}/{}.txt'.format(d, name) + txtname = f'{d}/{name}.txt' self.make_txt(txtname, txt) self.addCleanup(partial(self.delete_file, txtname)) blob = self._blob_for_name(name) - blobname = u'{}/{}.blob'.format(d, name) + blobname = f'{d}/{name}.blob' self.make_blob(blobname, blob) self.addCleanup(partial(self.delete_file, blobname)) @@ -241,10 +237,10 @@ def test_list_notebooks(self): self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'inspace.ipynb') - nbs = notebooks_only(self.api.list(u'/unicodé/').json()) + nbs = notebooks_only(self.api.list('/unicodé/').json()) self.assertEqual(len(nbs), 1) self.assertEqual(nbs[0]['name'], 'innonascii.ipynb') - self.assertEqual(nbs[0]['path'], u'unicodé/innonascii.ipynb') + self.assertEqual(nbs[0]['path'], 'unicodé/innonascii.ipynb') nbs = notebooks_only(self.api.list('/foo/bar/').json()) self.assertEqual(len(nbs), 1) @@ -254,7 +250,7 @@ def test_list_notebooks(self): nbs = notebooks_only(self.api.list('foo').json()) self.assertEqual(len(nbs), 4) nbnames = { normalize('NFC', n['name']) for n in nbs } - expected = [ u'a.ipynb', u'b.ipynb', u'name with spaces.ipynb', u'unicodé.ipynb'] + expected = [ 'a.ipynb', 'b.ipynb', 'name with spaces.ipynb', 'unicodé.ipynb'] expected = { normalize('NFC', name) for name in expected } self.assertEqual(nbnames, expected) @@ -284,7 +280,7 @@ def test_get_nb_contents(self): for d, name in self.dirs_nbs: path = url_path_join(d, name + '.ipynb') nb = self.api.read(path).json() - self.assertEqual(nb['name'], u'%s.ipynb' % name) + self.assertEqual(nb['name'], f'{name}.ipynb') self.assertEqual(nb['path'], path) self.assertEqual(nb['type'], 'notebook') self.assertIn('content', nb) @@ -296,7 +292,7 @@ def test_get_nb_no_content(self): for d, name in self.dirs_nbs: path = url_path_join(d, name + '.ipynb') nb = self.api.read(path, content=False).json() - self.assertEqual(nb['name'], u'%s.ipynb' % name) + self.assertEqual(nb['name'], f'{name}.ipynb') self.assertEqual(nb['path'], path) self.assertEqual(nb['type'], 'notebook') self.assertIn('content', nb) @@ -311,7 +307,7 @@ def test_get_nb_invalid(self): 'metadata': {}, }], } - path = u'å b/Validate tést.ipynb' + path = 'å b/Validate tést.ipynb' self.make_txt(path, py3compat.cast_unicode(json.dumps(nb))) model = self.api.read(path).json() self.assertEqual(model['path'], path) @@ -329,7 +325,7 @@ def test_get_text_file_contents(self): for d, name in self.dirs_nbs: path = url_path_join(d, name + '.txt') model = self.api.read(path).json() - self.assertEqual(model['name'], u'%s.txt' % name) + self.assertEqual(model['name'], f'{name}.txt') self.assertEqual(model['path'], path) self.assertIn('content', model) self.assertEqual(model['format'], 'text') @@ -348,7 +344,7 @@ def test_get_binary_file_contents(self): for d, name in self.dirs_nbs: path = url_path_join(d, name + '.blob') model = self.api.read(path).json() - self.assertEqual(model['name'], u'%s.blob' % name) + self.assertEqual(model['name'], f'{name}.blob') self.assertEqual(model['path'], path) self.assertIn('content', model) self.assertEqual(model['format'], 'base64') @@ -364,15 +360,15 @@ def test_get_binary_file_contents(self): def test_get_bad_type(self): with assert_http_error(400): - self.api.read(u'unicodé', type='file') # this is a directory + self.api.read('unicodé', type='file') # this is a directory with assert_http_error(400): - self.api.read(u'unicodé/innonascii.ipynb', type='directory') + self.api.read('unicodé/innonascii.ipynb', type='directory') def _check_created(self, resp, path, type='notebook'): self.assertEqual(resp.status_code, 201) location_header = py3compat.str_to_unicode(resp.headers['Location']) - self.assertEqual(location_header, url_path_join(self.url_prefix, u'api/contents', url_escape(path))) + self.assertEqual(location_header, url_path_join(self.url_prefix, 'api/contents', url_escape(path))) rjson = resp.json() self.assertEqual(rjson['name'], path.rsplit('/', 1)[-1]) self.assertEqual(rjson['path'], path) @@ -381,12 +377,12 @@ def _check_created(self, resp, path, type='notebook'): assert isright(path) def test_create_untitled(self): - resp = self.api.create_untitled(path=u'å b') - self._check_created(resp, u'å b/Untitled.ipynb') + resp = self.api.create_untitled(path='å b') + self._check_created(resp, 'å b/Untitled.ipynb') # Second time - resp = self.api.create_untitled(path=u'å b') - self._check_created(resp, u'å b/Untitled1.ipynb') + resp = self.api.create_untitled(path='å b') + self._check_created(resp, 'å b/Untitled1.ipynb') # And two directories down resp = self.api.create_untitled(path='foo/bar') @@ -405,39 +401,39 @@ def test_create_untitled_txt(self): def test_upload(self): nb = new_notebook() nbmodel = {'content': nb, 'type': 'notebook'} - path = u'å b/Upload tést.ipynb' + path = 'å b/Upload tést.ipynb' resp = self.api.upload(path, body=json.dumps(nbmodel)) self._check_created(resp, path) def test_mkdir_untitled(self): - resp = self.api.mkdir_untitled(path=u'å b') - self._check_created(resp, u'å b/Untitled Folder', type='directory') + resp = self.api.mkdir_untitled(path='å b') + self._check_created(resp, 'å b/Untitled Folder', type='directory') # Second time - resp = self.api.mkdir_untitled(path=u'å b') - self._check_created(resp, u'å b/Untitled Folder 1', type='directory') + resp = self.api.mkdir_untitled(path='å b') + self._check_created(resp, 'å b/Untitled Folder 1', type='directory') # And two directories down resp = self.api.mkdir_untitled(path='foo/bar') self._check_created(resp, 'foo/bar/Untitled Folder', type='directory') def test_mkdir(self): - path = u'å b/New ∂ir' + path = 'å b/New ∂ir' resp = self.api.mkdir(path) self._check_created(resp, path, type='directory') def test_mkdir_hidden_400(self): with assert_http_error(400): - resp = self.api.mkdir(u'å b/.hidden') + resp = self.api.mkdir('å b/.hidden') def test_upload_txt(self): - body = u'ünicode téxt' + body = 'ünicode téxt' model = { 'content' : body, 'format' : 'text', 'type' : 'file', } - path = u'å b/Upload tést.txt' + path = 'å b/Upload tést.txt' resp = self.api.upload(path, body=json.dumps(model)) # check roundtrip @@ -455,7 +451,7 @@ def test_upload_b64(self): 'format' : 'base64', 'type' : 'file', } - path = u'å b/Upload tést.blob' + path = 'å b/Upload tést.blob' resp = self.api.upload(path, body=json.dumps(model)) # check roundtrip @@ -473,7 +469,7 @@ def test_upload_v2(self): nb.worksheets.append(ws) ws.cells.append(v2.new_code_cell(input='print("hi")')) nbmodel = {'content': nb, 'type': 'notebook'} - path = u'å b/Upload tést.ipynb' + path = 'å b/Upload tést.ipynb' resp = self.api.upload(path, body=json.dumps(nbmodel)) self._check_created(resp, path) resp = self.api.read(path) @@ -481,38 +477,38 @@ def test_upload_v2(self): self.assertEqual(data['content']['nbformat'], 4) def test_copy(self): - resp = self.api.copy(u'å b/ç d.ipynb', u'å b') - self._check_created(resp, u'å b/ç d-Copy1.ipynb') - - resp = self.api.copy(u'å b/ç d.ipynb', u'å b') - self._check_created(resp, u'å b/ç d-Copy2.ipynb') - + resp = self.api.copy('å b/ç d.ipynb', 'å b') + self._check_created(resp, 'å b/ç d-Copy1.ipynb') + + resp = self.api.copy('å b/ç d.ipynb', 'å b') + self._check_created(resp, 'å b/ç d-Copy2.ipynb') + def test_copy_copy(self): - resp = self.api.copy(u'å b/ç d.ipynb', u'å b') - self._check_created(resp, u'å b/ç d-Copy1.ipynb') - - resp = self.api.copy(u'å b/ç d-Copy1.ipynb', u'å b') - self._check_created(resp, u'å b/ç d-Copy2.ipynb') - + resp = self.api.copy('å b/ç d.ipynb', 'å b') + self._check_created(resp, 'å b/ç d-Copy1.ipynb') + + resp = self.api.copy('å b/ç d-Copy1.ipynb', 'å b') + self._check_created(resp, 'å b/ç d-Copy2.ipynb') + def test_copy_path(self): - resp = self.api.copy(u'foo/a.ipynb', u'å b') - self._check_created(resp, u'å b/a.ipynb') - - resp = self.api.copy(u'foo/a.ipynb', u'å b') - self._check_created(resp, u'å b/a-Copy1.ipynb') + resp = self.api.copy('foo/a.ipynb', 'å b') + self._check_created(resp, 'å b/a.ipynb') + + resp = self.api.copy('foo/a.ipynb', 'å b') + self._check_created(resp, 'å b/a-Copy1.ipynb') def test_copy_put_400(self): with assert_http_error(400): - resp = self.api.copy_put(u'å b/ç d.ipynb', u'å b/cøpy.ipynb') + resp = self.api.copy_put('å b/ç d.ipynb', 'å b/cøpy.ipynb') def test_copy_dir_400(self): # can't copy directories with assert_http_error(400): - resp = self.api.copy(u'å b', u'foo') + resp = self.api.copy('å b', 'foo') def test_delete(self): for d, name in self.dirs_nbs: - print('%r, %r' % (d, name)) + print(f'{d!r}, {name!r}') resp = self.api.delete(url_path_join(d, name + '.ipynb')) self.assertEqual(resp.status_code, 204) @@ -537,15 +533,15 @@ def test_delete_non_empty_dir(self): self.skipTest("Disabled deleting non-empty dirs on Windows") # Test that non empty directory can be deleted try: - self.api.delete(u'å b') + self.api.delete('å b') except requests.HTTPError as e: if e.response.status_code == 400: - if not self.can_send2trash(u'å b'): + if not self.can_send2trash('å b'): self.skipTest("Dir can't be sent to trash") raise # Check if directory has actually been deleted with assert_http_error(404): - self.api.list(u'å b') + self.api.list('å b') def test_rename(self): resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb') @@ -555,7 +551,7 @@ def test_rename(self): assert self.isfile('foo/z.ipynb') nbs = notebooks_only(self.api.list('foo').json()) - nbnames = set(n['name'] for n in nbs) + nbnames = {n['name'] for n in nbs} self.assertIn('z.ipynb', nbnames) self.assertNotIn('a.ipynb', nbnames) @@ -599,7 +595,7 @@ def test_save(self): resp = self.api.read('foo/a.ipynb') nbcontent = json.loads(resp.text)['content'] nb = from_dict(nbcontent) - nb.cells.append(new_markdown_cell(u'Created by test ³')) + nb.cells.append(new_markdown_cell('Created by test ³')) nbmodel = {'content': nb, 'type': 'notebook'} resp = self.api.save('foo/a.ipynb', body=json.dumps(nbmodel)) @@ -607,7 +603,7 @@ def test_save(self): nbcontent = self.api.read('foo/a.ipynb').json()['content'] newnb = from_dict(nbcontent) self.assertEqual(newnb.cells[0].source, - u'Created by test ³') + 'Created by test ³') def test_checkpoints(self): resp = self.api.read('foo/a.ipynb') diff --git a/notebook/services/contents/tests/test_fileio.py b/notebook/services/contents/tests/test_fileio.py index adc06d97f1..281be6a3d6 100644 --- a/notebook/services/contents/tests/test_fileio.py +++ b/notebook/services/contents/tests/test_fileio.py @@ -3,7 +3,6 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -import io as stdlib_io import os.path import unittest import pytest @@ -17,13 +16,14 @@ umask = 0 def test_atomic_writing(): - class CustomExc(Exception): pass + class CustomExc(Exception): + pass with TemporaryDirectory() as td: f1 = os.path.join(td, 'penguin') - with stdlib_io.open(f1, 'w') as f: - f.write(u'Before') - + with open(f1, 'w') as f: + f.write('Before') + if os.name != 'nt': os.chmod(f1, 0o701) orig_mode = stat.S_IMODE(os.stat(f1).st_mode) @@ -40,18 +40,18 @@ class CustomExc(Exception): pass with pytest.raises(CustomExc): with atomic_writing(f1) as f: - f.write(u'Failing write') + f.write('Failing write') raise CustomExc # Because of the exception, the file should not have been modified - with stdlib_io.open(f1, 'r') as f: - assert f.read() == u'Before' + with open(f1) as f: + assert f.read() == 'Before' with atomic_writing(f1) as f: - f.write(u'Overwritten') + f.write('Overwritten') - with stdlib_io.open(f1, 'r') as f: - assert f.read() == u'Overwritten' + with open(f1) as f: + assert f.read() == 'Overwritten' if os.name != 'nt': mode = stat.S_IMODE(os.stat(f1).st_mode) @@ -60,10 +60,10 @@ class CustomExc(Exception): pass if have_symlink: # Check that writing over a file preserves a symlink with atomic_writing(f2) as f: - f.write(u'written from symlink') - - with stdlib_io.open(f1, 'r') as f: - assert f.read() == u'written from symlink' + f.write('written from symlink') + + with open(f1) as f: + assert f.read() == 'written from symlink' class TestWithSetUmask(unittest.TestCase): def setUp(self): @@ -71,7 +71,7 @@ def setUp(self): global umask umask = os.umask(0) os.umask(umask) - + def tearDown(self): # restore umask os.umask(umask) @@ -82,14 +82,14 @@ def test_atomic_writing_umask(self): os.umask(0o022) f1 = os.path.join(td, '1') with atomic_writing(f1) as f: - f.write(u'1') + f.write('1') mode = stat.S_IMODE(os.stat(f1).st_mode) assert mode == 0o644 - + os.umask(0o057) f2 = os.path.join(td, '2') with atomic_writing(f2) as f: - f.write(u'2') + f.write('2') mode = stat.S_IMODE(os.stat(f2).st_mode) assert mode == 0o620 @@ -97,36 +97,36 @@ def test_atomic_writing_umask(self): def test_atomic_writing_newlines(): with TemporaryDirectory() as td: path = os.path.join(td, 'testfile') - - lf = u'a\nb\nc\n' - plat = lf.replace(u'\n', os.linesep) - crlf = lf.replace(u'\n', u'\r\n') - + + lf = 'a\nb\nc\n' + plat = lf.replace('\n', os.linesep) + crlf = lf.replace('\n', '\r\n') + # test default - with stdlib_io.open(path, 'w') as f: + with open(path, 'w') as f: f.write(lf) - with stdlib_io.open(path, 'r', newline='') as f: + with open(path, newline='') as f: read = f.read() assert read == plat - + # test newline=LF - with stdlib_io.open(path, 'w', newline='\n') as f: + with open(path, 'w', newline='\n') as f: f.write(lf) - with stdlib_io.open(path, 'r', newline='') as f: + with open(path, newline='') as f: read = f.read() assert read == lf - + # test newline=CRLF with atomic_writing(path, newline='\r\n') as f: f.write(lf) - with stdlib_io.open(path, 'r', newline='') as f: + with open(path, newline='') as f: read = f.read() assert read == crlf - + # test newline=no convert - text = u'crlf\r\ncr\rlf\n' + text = 'crlf\r\ncr\rlf\n' with atomic_writing(path, newline='') as f: f.write(text) - with stdlib_io.open(path, 'r', newline='') as f: + with open(path, newline='') as f: read = f.read() assert read == text diff --git a/notebook/services/contents/tests/test_largefilemanager.py b/notebook/services/contents/tests/test_largefilemanager.py index 13d294b9b0..d52c697acb 100644 --- a/notebook/services/contents/tests/test_largefilemanager.py +++ b/notebook/services/contents/tests/test_largefilemanager.py @@ -13,7 +13,7 @@ def _make_dir(contents_manager, api_path): try: os.makedirs(os_path) except OSError: - print("Directory already exists: %r" % os_path) + print(f"Directory already exists: {os_path!r}") class TestLargeFileManager(TestCase): @@ -68,7 +68,7 @@ def test_save(self): try: model = {'name': 'test', 'path': 'test', 'chunk': 2, 'type': 'file', - 'content': u'test', 'format': 'json'} + 'content': 'test', 'format': 'json'} cm.save(model, model['path']) except web.HTTPError as e: self.assertEqual("HTTP 400: Bad Request (Must specify format of file contents as 'text' or 'base64')", @@ -76,7 +76,7 @@ def test_save(self): # Save model for different chunks model = {'name': 'test', 'path': 'test', 'type': 'file', - 'content': u'test==', 'format': 'text'} + 'content': 'test==', 'format': 'text'} name = model['name'] path = model['path'] cm.save(model, path) diff --git a/notebook/services/contents/tests/test_manager.py b/notebook/services/contents/tests/test_manager.py index dfe5d272f5..cac98e893d 100644 --- a/notebook/services/contents/tests/test_manager.py +++ b/notebook/services/contents/tests/test_manager.py @@ -26,14 +26,14 @@ def _make_dir(contents_manager, api_path): try: os.makedirs(os_path) except OSError: - print("Directory already exists: %r" % os_path) + print(f"Directory already exists: {os_path!r}") class TestFileContentsManager(TestCase): @contextmanager def assertRaisesHTTPError(self, status, msg=None): - msg = msg or "Should have raised HTTPError(%i)" % status + msg = msg or f"Should have raised HTTPError({status})" try: yield except HTTPError as e: @@ -87,7 +87,7 @@ def test_get_os_path(self): self.assertEqual(path, fs_path) def test_checkpoint_subdir(self): - subd = u'sub ∂ir' + subd = 'sub ∂ir' cp_name = 'test-cp.ipynb' with TemporaryDirectory() as td: root = td @@ -98,7 +98,7 @@ def test_checkpoint_subdir(self): 'cp', 'test.ipynb' ) cp_subdir = cpm.checkpoint_path( - 'cp', '/%s/test.ipynb' % subd + 'cp', f'/{subd}/test.ipynb' ) self.assertNotEqual(cp_dir, cp_subdir) self.assertEqual(cp_dir, os.path.join(root, cpm.checkpoint_dir, cp_name)) @@ -113,7 +113,7 @@ def test_bad_symlink(self): file_model = cm.new_untitled(path=path, ext='.txt') # create a broken symlink - self.symlink(cm, "target", '%s/%s' % (path, 'bad symlink')) + self.symlink(cm, "target", f'{path}/{"bad symlink"}') model = cm.get(path) contents = { @@ -149,7 +149,7 @@ def test_good_symlink(self): cm = FileContentsManager(root_dir=td) parent = 'test good symlink' name = 'good symlink' - path = '{0}/{1}'.format(parent, name) + path = f'{parent}/{name}' _make_dir(cm, parent) file_model = cm.new(path=parent + '/zfoo.txt') @@ -175,7 +175,7 @@ def test_403(self): os.chmod(os_path, 0o400) try: with cm.open(os_path, 'w') as f: - f.write(u"don't care") + f.write("don't care") except HTTPError as e: self.assertEqual(e.status_code, 403) else: @@ -201,7 +201,7 @@ def test_escape_root(self): with self.assertRaisesHTTPError(404): cm.save(model={ 'type': 'file', - 'content': u'', + 'content': '', 'format': 'text', }, path='../foo') @@ -209,7 +209,7 @@ def test_escape_root(self): class TestContentsManager(TestCase): @contextmanager def assertRaisesHTTPError(self, status, msg=None): - msg = msg or "Should have raised HTTPError(%i)" % status + msg = msg or f"Should have raised HTTPError({status})" try: yield except HTTPError as e: @@ -309,7 +309,7 @@ def test_new_untitled(self): self.assertIn('type', model) self.assertEqual(model['type'], 'file') self.assertEqual(model['name'], 'untitled') - self.assertEqual(model['path'], '%s/untitled' % sub_dir) + self.assertEqual(model['path'], f'{sub_dir}/untitled') # Test with a compound extension model = cm.new_untitled(path=sub_dir, ext='.foo.bar') @@ -378,19 +378,19 @@ def test_get(self): self.assertIn('path', model2) self.assertIn('content', model2) self.assertEqual(model2['name'], 'Untitled.ipynb') - self.assertEqual(model2['path'], '{0}/{1}'.format(sub_dir.strip('/'), name)) + self.assertEqual(model2['path'], f'{sub_dir.strip("/")}/{name}') # Test with a regular file. file_model_path = cm.new_untitled(path=sub_dir, ext='.txt')['path'] file_model = cm.get(file_model_path) self.assertDictContainsSubset( { - 'content': u'', - 'format': u'text', - 'mimetype': u'text/plain', - 'name': u'untitled.txt', - 'path': u'foo/untitled.txt', - 'type': u'file', + 'content': '', + 'format': 'text', + 'mimetype': 'text/plain', + 'name': 'untitled.txt', + 'path': 'foo/untitled.txt', + 'type': 'file', 'writable': True, }, file_model, @@ -413,7 +413,7 @@ def test_get(self): # Directory contents should match the contents of each individual entry # when requested with content=False. model2_no_content = cm.get(sub_dir + name, content=False) - file_model_no_content = cm.get(u'foo/untitled.txt', content=False) + file_model_no_content = cm.get('foo/untitled.txt', content=False) sub_sub_dir_no_content = cm.get('foo/bar', content=False) self.assertEqual(sub_sub_dir_no_content['path'], 'foo/bar') self.assertEqual(sub_sub_dir_no_content['name'], 'bar') @@ -428,7 +428,7 @@ def test_get(self): elif entry['path'] == file_model_no_content['path']: self.assertEqual(entry, file_model_no_content) else: - self.fail("Unexpected directory entry: %s" % entry()) + self.fail(f"Unexpected directory entry: {entry()}") with self.assertRaises(HTTPError): cm.get('foo', type='file') @@ -582,9 +582,9 @@ def test_delete_root(self): def test_copy(self): cm = self.contents_manager - parent = u'å b' - name = u'nb √.ipynb' - path = u'{0}/{1}'.format(parent, name) + parent = 'å b' + name = 'nb √.ipynb' + path = f'{parent}/{name}' self.make_dir(parent) orig = cm.new(path=path) @@ -593,11 +593,11 @@ def test_copy(self): self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy1.ipynb')) # copy with specified name - copy2 = cm.copy(path, u'å b/copy 2.ipynb') - self.assertEqual(copy2['name'], u'copy 2.ipynb') - self.assertEqual(copy2['path'], u'å b/copy 2.ipynb') + copy2 = cm.copy(path, 'å b/copy 2.ipynb') + self.assertEqual(copy2['name'], 'copy 2.ipynb') + self.assertEqual(copy2['path'], 'å b/copy 2.ipynb') # copy with specified path - copy2 = cm.copy(path, u'/') + copy2 = cm.copy(path, '/') self.assertEqual(copy2['name'], name) self.assertEqual(copy2['path'], name) diff --git a/notebook/services/kernels/handlers.py b/notebook/services/kernels/handlers.py index a2d4ddbab9..d2c5f8ef72 100644 --- a/notebook/services/kernels/handlers.py +++ b/notebook/services/kernels/handlers.py @@ -124,7 +124,7 @@ def rate_limit_window(self): return self.settings.get('rate_limit_window', 1.0) def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized')) + return f"{self.__class__.__name__}({getattr(self, 'kernel_id', 'uninitialized')})" def create_stream(self): km = self.kernel_manager @@ -252,7 +252,7 @@ def nudge(count): if not both_done.done(): log = self.log.warning if count % 10 == 0 else self.log.debug - log("Nudge: attempt %s on kernel %s" % (count, self.kernel_id)) + log(f"Nudge: attempt {count} on kernel {self.kernel_id}") self.session.send(shell_channel, "kernel_info_request") self.session.send(control_channel, "kernel_info_request") nonlocal nudge_handle @@ -323,7 +323,7 @@ def _finish_kernel_info(self, info): protocol_version = info.get('protocol_version', client_protocol_version) if protocol_version != client_protocol_version: self.session.adapt_version = int(protocol_version.split('.')[0]) - self.log.info("Adapting from protocol version {protocol_version} (kernel {kernel_id}) to {client_protocol_version} (client).".format(protocol_version=protocol_version, kernel_id=self.kernel_id, client_protocol_version=client_protocol_version)) + self.log.info(f"Adapting from protocol version {protocol_version} (kernel {self.kernel_id}) to {client_protocol_version} (client).") if not self._kernel_info_future.done(): self._kernel_info_future.set_result(info) @@ -384,7 +384,7 @@ def _register_session(self): This is likely due to a client reconnecting from a lost network connection, where the socket on our side has not been cleaned up yet. """ - self.session_key = '%s:%s' % (self.kernel_id, self.session.session) + self.session_key = f'{self.kernel_id}:{self.session.session}' stale_handler = self._open_sessions.get(self.session_key) if stale_handler: self.log.warning("Replacing stale connection: %s", self.session_key) @@ -457,7 +457,7 @@ def on_message(self, msg): am = self.kernel_manager.allowed_message_types mt = msg['header']['msg_type'] if am and mt not in am: - self.log.warning('Received message of type "%s", which is not allowed. Ignoring.' % mt) + self.log.warning(f'Received message of type "{mt}", which is not allowed. Ignoring.') else: stream = self.channels[channel] self.session.send(stream, msg) @@ -504,7 +504,7 @@ def write_stderr(error_message): # Increment the bytes and message count self._iopub_window_msg_count += 1 if msg_type == 'stream': - byte_count = sum([len(x) for x in msg_list]) + byte_count = sum(len(x) for x in msg_list) else: byte_count = 0 self._iopub_window_byte_count += byte_count @@ -522,7 +522,7 @@ def write_stderr(error_message): if self.iopub_msg_rate_limit > 0 and msg_rate > self.iopub_msg_rate_limit: if not self._iopub_msgs_exceeded: self._iopub_msgs_exceeded = True - write_stderr(dedent("""\ + write_stderr(dedent(f"""\ IOPub message rate exceeded. The notebook server will temporarily stop sending output to the client in order to avoid crashing it. @@ -530,9 +530,9 @@ def write_stderr(error_message): `--NotebookApp.iopub_msg_rate_limit`. Current values: - NotebookApp.iopub_msg_rate_limit={} (msgs/sec) - NotebookApp.rate_limit_window={} (secs) - """.format(self.iopub_msg_rate_limit, self.rate_limit_window))) + NotebookApp.iopub_msg_rate_limit={self.iopub_msg_rate_limit} (msgs/sec) + NotebookApp.rate_limit_window={self.rate_limit_window} (secs) + """)) else: # resume once we've got some headroom below the limit if self._iopub_msgs_exceeded and msg_rate < (0.8 * self.iopub_msg_rate_limit): @@ -544,7 +544,7 @@ def write_stderr(error_message): if self.iopub_data_rate_limit > 0 and data_rate > self.iopub_data_rate_limit: if not self._iopub_data_exceeded: self._iopub_data_exceeded = True - write_stderr(dedent("""\ + write_stderr(dedent(f"""\ IOPub data rate exceeded. The notebook server will temporarily stop sending output to the client in order to avoid crashing it. @@ -552,9 +552,9 @@ def write_stderr(error_message): `--NotebookApp.iopub_data_rate_limit`. Current values: - NotebookApp.iopub_data_rate_limit={} (bytes/sec) - NotebookApp.rate_limit_window={} (secs) - """.format(self.iopub_data_rate_limit, self.rate_limit_window))) + NotebookApp.iopub_data_rate_limit={self.iopub_data_rate_limit} (bytes/sec) + NotebookApp.rate_limit_window={self.rate_limit_window} (secs) + """)) else: # resume once we've got some headroom below the limit if self._iopub_data_exceeded and data_rate < (0.8 * self.iopub_data_rate_limit): @@ -640,7 +640,7 @@ def on_restart_failed(self): default_handlers = [ (r"/api/kernels", MainKernelHandler), - (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler), - (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler), - (r"/api/kernels/%s/channels" % _kernel_id_regex, ZMQChannelsHandler), + (fr"/api/kernels/{_kernel_id_regex}", KernelHandler), + (fr"/api/kernels/{_kernel_id_regex}/{_kernel_action_regex}", KernelActionHandler), + (fr"/api/kernels/{_kernel_id_regex}/channels", ZMQChannelsHandler), ] diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index 7ed182dfc0..037c42be0e 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -33,7 +33,7 @@ try: from jupyter_client.multikernelmanager import AsyncMultiKernelManager except ImportError: - class AsyncMultiKernelManager(object): + class AsyncMultiKernelManager: """Empty class to satisfy unused reference by AsyncMappingKernelManager.""" def __init__(self, **kwargs): pass @@ -71,7 +71,7 @@ def _update_root_dir(self, proposal): # If we receive a non-absolute path, make it absolute. value = os.path.abspath(value) if not exists(value) or not os.path.isdir(value): - raise TraitError("kernel root dir %r is not a directory" % value) + raise TraitError(f"kernel root dir {value!r} is not a directory") return value cull_idle_timeout = Integer(0, config=True, @@ -176,8 +176,8 @@ async def start_kernel(self, kernel_id=None, path=None, **kwargs): kernel_id = await maybe_future(self.pinned_superclass.start_kernel(self, **kwargs)) self._kernel_connections[kernel_id] = 0 self.start_watching_activity(kernel_id) - self.log.info("Kernel started: %s, name: %s" % (kernel_id, self._kernels[kernel_id].kernel_name)) - self.log.debug("Kernel args: %r" % kwargs) + self.log.info(f"Kernel started: {kernel_id}, name: {self._kernels[kernel_id].kernel_name}") + self.log.debug(f"Kernel args: {kwargs!r}") # register callback for failed auto-restart self.add_restart_callback(kernel_id, lambda : self._handle_kernel_died(kernel_id), @@ -192,7 +192,7 @@ async def start_kernel(self, kernel_id=None, path=None, **kwargs): else: self._check_kernel_id(kernel_id) - self.log.info("Using existing kernel: %s" % kernel_id) + self.log.info(f"Using existing kernel: {kernel_id}") # Initialize culling if not already if not self._initialized_culler: @@ -391,7 +391,7 @@ def list_kernels(self): def _check_kernel_id(self, kernel_id): """Check a that a kernel_id exists and raise 404 if not.""" if kernel_id not in self: - raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id) + raise web.HTTPError(404, f'Kernel does not exist: {kernel_id}') # monitoring activity: @@ -457,8 +457,10 @@ async def cull_kernels(self): try: await self.cull_kernel_if_idle(kernel_id) except Exception as e: - self.log.exception("The following exception was encountered while checking the " - "idle duration of kernel {}: {}".format(kernel_id, e)) + self.log.exception( + f"The following exception was encountered while checking the idle duration of kernel " + f"{kernel_id}: {e}" + ) async def cull_kernel_if_idle(self, kernel_id): try: diff --git a/notebook/services/kernels/tests/test_kernels_api.py b/notebook/services/kernels/tests/test_kernels_api.py index 7895f36a81..38a9dfb00c 100644 --- a/notebook/services/kernels/tests/test_kernels_api.py +++ b/notebook/services/kernels/tests/test_kernels_api.py @@ -1,7 +1,6 @@ """Test the kernels service API.""" import json -import sys import time from requests import HTTPError @@ -24,7 +23,7 @@ async_testing_enabled = False -class KernelAPI(object): +class KernelAPI: """Wrapper for kernel REST API requests""" def __init__(self, request, base_url, headers): self.request = request @@ -204,11 +203,11 @@ class AsyncKernelAPITest(KernelAPITest): def setup_class(cls): if not async_testing_enabled: # Can be removed once jupyter_client >= 6.1 is required. raise SkipTest("AsyncKernelAPITest tests skipped due to down-level jupyter_client!") - super(AsyncKernelAPITest, cls).setup_class() + super().setup_class() @classmethod def get_argv(cls): - argv = super(AsyncKernelAPITest, cls).get_argv() + argv = super().get_argv() # before we extend the argv with the class, ensure that appropriate jupyter_client is available. # if not available, don't set kernel_manager_class, resulting in the repeat of sync-based tests. @@ -244,11 +243,11 @@ class KernelCullingTest(NotebookTestBase): @classmethod def get_argv(cls): - argv = super(KernelCullingTest, cls).get_argv() + argv = super().get_argv() # Enable culling with 5s timeout and 1s intervals - argv.extend(['--MappingKernelManager.cull_idle_timeout={}'.format(CULL_TIMEOUT), - '--MappingKernelManager.cull_interval={}'.format(CULL_INTERVAL), + argv.extend([f'--MappingKernelManager.cull_idle_timeout={CULL_TIMEOUT}', + f'--MappingKernelManager.cull_interval={CULL_INTERVAL}', '--MappingKernelManager.cull_connected=False']) return argv diff --git a/notebook/services/kernelspecs/handlers.py b/notebook/services/kernelspecs/handlers.py index a01d307fb2..bc68f367c8 100644 --- a/notebook/services/kernelspecs/handlers.py +++ b/notebook/services/kernelspecs/handlers.py @@ -87,7 +87,7 @@ def get(self, kernel_name): try: spec = yield maybe_future(ksm.get_kernel_spec(kernel_name)) except KeyError as e: - raise web.HTTPError(404, u'Kernel spec %s not found' % kernel_name) from e + raise web.HTTPError(404, f'Kernel spec {kernel_name} not found') from e if is_kernelspec_model(spec): model = spec else: @@ -102,5 +102,5 @@ def get(self, kernel_name): default_handlers = [ (r"/api/kernelspecs", MainKernelSpecHandler), - (r"/api/kernelspecs/%s" % kernel_name_regex, KernelSpecHandler), + (fr"/api/kernelspecs/{kernel_name_regex}", KernelSpecHandler), ] diff --git a/notebook/services/kernelspecs/tests/test_kernelspecs_api.py b/notebook/services/kernelspecs/tests/test_kernelspecs_api.py index 215bfc861b..846743933d 100644 --- a/notebook/services/kernelspecs/tests/test_kernelspecs_api.py +++ b/notebook/services/kernelspecs/tests/test_kernelspecs_api.py @@ -1,17 +1,14 @@ """Test the kernel specs webservice API.""" import errno -import io import json import os import shutil pjoin = os.path.join -import requests - from jupyter_client.kernelspec import NATIVE_KERNEL_NAME -from notebook.utils import url_path_join, url_escape +from notebook.utils import url_path_join from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error # Copied from jupyter_client.tests.test_kernelspec so updating that doesn't @@ -20,10 +17,10 @@ 'display_name':'Test kernel', } -some_resource = u"The very model of a modern major general" +some_resource = "The very model of a modern major general" -class KernelSpecAPI(object): +class KernelSpecAPI: """Wrapper for notebook API calls.""" def __init__(self, request): self.request = request @@ -41,7 +38,7 @@ def list(self): def kernel_spec_info(self, name): return self._req('GET', url_path_join('api/kernelspecs', name)) - + def kernel_resource(self, name, path): return self._req('GET', url_path_join('kernelspecs', name, path)) @@ -60,11 +57,11 @@ def create_spec(self, name): except OSError as e: if e.errno != errno.EEXIST: raise - + with open(pjoin(sample_kernel_dir, 'kernel.json'), 'w') as f: json.dump(sample_kernel_json, f) - - with io.open(pjoin(sample_kernel_dir, 'resource.txt'), 'w', + + with open(pjoin(sample_kernel_dir, 'resource.txt'), 'w', encoding='utf-8') as f: f.write(some_resource) @@ -76,10 +73,10 @@ def test_list_kernelspecs_bad(self): except OSError as e: if e.errno != errno.EEXIST: raise - + with open(pjoin(bad_kernel_dir, 'kernel.json'), 'w') as f: f.write("garbage") - + model = self.ks_api.list().json() assert isinstance(model, dict) self.assertEqual(model['default'], NATIVE_KERNEL_NAME) @@ -87,9 +84,9 @@ def test_list_kernelspecs_bad(self): assert isinstance(specs, dict) # 2: the sample kernelspec created in setUp, and the native Python kernel self.assertGreaterEqual(len(specs), 2) - + shutil.rmtree(bad_kernel_dir) - + def test_list_kernelspecs(self): model = self.ks_api.list().json() assert isinstance(model, dict) @@ -123,14 +120,14 @@ def test_get_kernelspec_spaces(self): def test_get_nonexistant_kernelspec(self): with assert_http_error(404): self.ks_api.kernel_spec_info('nonexistant') - + def test_get_kernel_resource_file(self): res = self.ks_api.kernel_resource('sAmple', 'resource.txt') self.assertEqual(res.text, some_resource) - + def test_get_nonexistant_resource(self): with assert_http_error(404): self.ks_api.kernel_resource('nonexistant', 'resource.txt') - + with assert_http_error(404): self.ks_api.kernel_resource('sample', 'nonexistant.txt') diff --git a/notebook/services/nbconvert/handlers.py b/notebook/services/nbconvert/handlers.py index 7c65ee0fd9..563381c91b 100644 --- a/notebook/services/nbconvert/handlers.py +++ b/notebook/services/nbconvert/handlers.py @@ -13,7 +13,7 @@ def get(self): try: from nbconvert.exporters import base except ImportError as e: - raise web.HTTPError(500, "Could not import nbconvert: %s" % e) from e + raise web.HTTPError(500, f"Could not import nbconvert: {e}") from e res = {} exporters = base.get_export_names() for exporter_name in exporters: diff --git a/notebook/services/nbconvert/tests/test_nbconvert_api.py b/notebook/services/nbconvert/tests/test_nbconvert_api.py index d6ef9d2ca5..b7e9059dc0 100644 --- a/notebook/services/nbconvert/tests/test_nbconvert_api.py +++ b/notebook/services/nbconvert/tests/test_nbconvert_api.py @@ -1,9 +1,7 @@ -import requests - from notebook.utils import url_path_join from notebook.tests.launchnotebook import NotebookTestBase -class NbconvertAPI(object): +class NbconvertAPI: """Wrapper for nbconvert API calls.""" def __init__(self, request): self.request = request diff --git a/notebook/services/sessions/handlers.py b/notebook/services/sessions/handlers.py index fc4b3d1d38..7c0540fd6b 100644 --- a/notebook/services/sessions/handlers.py +++ b/notebook/services/sessions/handlers.py @@ -76,10 +76,12 @@ def post(self): kernel_id=kernel_id, name=name, type=mtype)) except NoSuchKernel: - msg = ("The '%s' kernel is not available. Please pick another " - "suitable kernel instead, or install that kernel." % kernel_name) - status_msg = '%s not found' % kernel_name - self.log.warning('Kernel not found: %s' % kernel_name) + msg = ( + f"The '{kernel_name}' kernel is not available. " + f"Please pick another suitable kernel instead, or install that kernel." + ) + status_msg = f'{kernel_name} not found' + self.log.warning(f'Kernel not found: {kernel_name}') self.set_status(501) self.finish(json.dumps(dict(message=msg, short_message=status_msg))) return @@ -133,7 +135,7 @@ def patch(self, session_id): if model['kernel'].get('id') is not None: kernel_id = model['kernel']['id'] if kernel_id not in km: - raise web.HTTPError(400, "No such kernel: %s" % kernel_id) + raise web.HTTPError(400, f"No such kernel: {kernel_id}") changes['kernel_id'] = kernel_id elif model['kernel'].get('name') is not None: kernel_name = model['kernel']['name'] @@ -174,7 +176,7 @@ def delete(self, session_id): _session_id_regex = r"(?P\w+-\w+-\w+-\w+-\w+)" default_handlers = [ - (r"/api/sessions/%s" % _session_id_regex, SessionHandler), + (fr"/api/sessions/{_session_id_regex}", SessionHandler), (r"/api/sessions", SessionRootHandler) ] diff --git a/notebook/services/sessions/sessionmanager.py b/notebook/services/sessions/sessionmanager.py index 5686332868..206090839a 100644 --- a/notebook/services/sessions/sessionmanager.py +++ b/notebook/services/sessions/sessionmanager.py @@ -171,9 +171,9 @@ def get_session(self, **kwargs): for column in kwargs.keys(): if column not in self._columns: raise TypeError("No such column: %r", column) - conditions.append("%s=?" % column) + conditions.append(f"{column}=?") - query = "SELECT * FROM session WHERE %s" % (' AND '.join(conditions)) + query = f"SELECT * FROM session WHERE {' AND '.join(conditions)}" self.cursor.execute(query, list(kwargs.values())) try: @@ -185,14 +185,14 @@ def get_session(self, **kwargs): if row is None: q = [] for key, value in kwargs.items(): - q.append("%s=%r" % (key, value)) + q.append(f"{key}={value!r}") - raise web.HTTPError(404, u'Session not found: %s' % (', '.join(q))) + raise web.HTTPError(404, f'Session not found: {", ".join(q)}') try: model = yield maybe_future(self.row_to_model(row)) except KeyError as e: - raise web.HTTPError(404, u'Session not found: %s' % str(e)) + raise web.HTTPError(404, f'Session not found: {e}') raise gen.Return(model) @gen.coroutine @@ -220,9 +220,9 @@ def update_session(self, session_id, **kwargs): sets = [] for column in kwargs.keys(): if column not in self._columns: - raise TypeError("No such column: %r" % column) - sets.append("%s=?" % column) - query = "UPDATE session SET %s WHERE session_id=?" % (', '.join(sets)) + raise TypeError(f"No such column: {column!r}") + sets.append(f"{column}=?") + query = f"UPDATE session SET {', '.join(sets)} WHERE session_id=?" self.cursor.execute(query, list(kwargs.values()) + [session_id]) def kernel_culled(self, kernel_id): diff --git a/notebook/services/sessions/tests/test_sessionmanager.py b/notebook/services/sessions/tests/test_sessionmanager.py index 9af38033c8..b58f8f51d5 100644 --- a/notebook/services/sessions/tests/test_sessionmanager.py +++ b/notebook/services/sessions/tests/test_sessionmanager.py @@ -11,7 +11,7 @@ from notebook.services.contents.manager import ContentsManager from notebook._tz import utcnow, isoformat -class DummyKernel(object): +class DummyKernel: def __init__(self, kernel_name='python'): self.kernel_name = kernel_name @@ -22,11 +22,11 @@ class DummyMKM(MappingKernelManager): """MappingKernelManager interface that doesn't start kernels, for testing""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.id_letters = iter(u'ABCDEFGHIJK') + self.id_letters = iter('ABCDEFGHIJK') def _new_id(self): return next(self.id_letters) - + def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs): kernel_id = kernel_id or self._new_id() k = self._kernels[kernel_id] = DummyKernel(kernel_name=kernel_name) @@ -40,7 +40,7 @@ def shutdown_kernel(self, kernel_id, now=False): class TestSessionManager(TestCase): - + def setUp(self): self.sm = SessionManager( kernel_manager=DummyMKM(), @@ -59,7 +59,7 @@ def co_add(): sessions.append(session) raise gen.Return(sessions) return self.loop.run_sync(co_add) - + def create_session(self, **kwargs): return self.create_sessions(kwargs)[0] @@ -68,8 +68,8 @@ def test_get_session(self): session_id = self.create_session(path='/path/to/test.ipynb', kernel_name='bar')['id'] model = self.loop.run_sync(lambda: sm.get_session(session_id=session_id)) expected = {'id':session_id, - 'path': u'/path/to/test.ipynb', - 'notebook': {'path': u'/path/to/test.ipynb', 'name': None}, + 'path': '/path/to/test.ipynb', + 'notebook': {'path': '/path/to/test.ipynb', 'name': None}, 'type': 'notebook', 'name': None, 'kernel': { @@ -103,18 +103,18 @@ def test_get_session_dead_kernel(self): def test_list_sessions(self): sm = self.sm sessions = self.create_sessions( - dict(path='/path/to/1/test1.ipynb', kernel_name='python'), - dict(path='/path/to/2/test2.py', type='file', kernel_name='python'), - dict(path='/path/to/3', name='foo', type='console', kernel_name='python'), + {'path': '/path/to/1/test1.ipynb', 'kernel_name': 'python'}, + {'path': '/path/to/2/test2.py', 'type': 'file', 'kernel_name': 'python'}, + {'path': '/path/to/3', 'name': 'foo', 'type': 'console', 'kernel_name': 'python'}, ) - + sessions = self.loop.run_sync(lambda: sm.list_sessions()) expected = [ { 'id':sessions[0]['id'], - 'path': u'/path/to/1/test1.ipynb', + 'path': '/path/to/1/test1.ipynb', 'type': 'notebook', - 'notebook': {'path': u'/path/to/1/test1.ipynb', 'name': None}, + 'notebook': {'path': '/path/to/1/test1.ipynb', 'name': None}, 'name': None, 'kernel': { 'id': 'A', @@ -125,7 +125,7 @@ def test_list_sessions(self): } }, { 'id':sessions[1]['id'], - 'path': u'/path/to/2/test2.py', + 'path': '/path/to/2/test2.py', 'type': 'file', 'name': None, 'kernel': { @@ -137,7 +137,7 @@ def test_list_sessions(self): } }, { 'id':sessions[2]['id'], - 'path': u'/path/to/3', + 'path': '/path/to/3', 'type': 'console', 'name': 'foo', 'kernel': { @@ -163,10 +163,10 @@ def test_list_sessions_dead_kernel(self): expected = [ { 'id': sessions[1]['id'], - 'path': u'/path/to/2/test2.ipynb', + 'path': '/path/to/2/test2.ipynb', 'type': 'notebook', 'name': None, - 'notebook': {'path': u'/path/to/2/test2.ipynb', 'name': None}, + 'notebook': {'path': '/path/to/2/test2.ipynb', 'name': None}, 'kernel': { 'id': 'B', 'name':'python', @@ -185,10 +185,10 @@ def test_update_session(self): self.loop.run_sync(lambda: sm.update_session(session_id, path='/path/to/new_name.ipynb')) model = self.loop.run_sync(lambda: sm.get_session(session_id=session_id)) expected = {'id':session_id, - 'path': u'/path/to/new_name.ipynb', + 'path': '/path/to/new_name.ipynb', 'type': 'notebook', 'name': None, - 'notebook': {'path': u'/path/to/new_name.ipynb', 'name': None}, + 'notebook': {'path': '/path/to/new_name.ipynb', 'name': None}, 'kernel': { 'id': 'A', 'name':'julia', @@ -218,10 +218,10 @@ def test_delete_session(self): new_sessions = self.loop.run_sync(lambda: sm.list_sessions()) expected = [{ 'id': sessions[0]['id'], - 'path': u'/path/to/1/test1.ipynb', + 'path': '/path/to/1/test1.ipynb', 'type': 'notebook', 'name': None, - 'notebook': {'path': u'/path/to/1/test1.ipynb', 'name': None}, + 'notebook': {'path': '/path/to/1/test1.ipynb', 'name': None}, 'kernel': { 'id': 'A', 'name':'python', @@ -232,7 +232,7 @@ def test_delete_session(self): }, { 'id': sessions[2]['id'], 'type': 'console', - 'path': u'/path/to/3', + 'path': '/path/to/3', 'name': 'foo', 'kernel': { 'id': 'C', diff --git a/notebook/services/sessions/tests/test_sessions_api.py b/notebook/services/sessions/tests/test_sessions_api.py index cb4bc0bdea..67321e5e34 100644 --- a/notebook/services/sessions/tests/test_sessions_api.py +++ b/notebook/services/sessions/tests/test_sessions_api.py @@ -2,11 +2,9 @@ import errno from functools import partial -import io import os import json import shutil -import sys import time from unittest import SkipTest @@ -25,7 +23,7 @@ pjoin = os.path.join -class SessionAPI(object): +class SessionAPI: """Wrapper for notebook API calls.""" def __init__(self, request): self.request = request @@ -101,7 +99,7 @@ def setUp(self): raise self.addCleanup(partial(shutil.rmtree, subdir, ignore_errors=True)) - with io.open(pjoin(subdir, 'nb1.ipynb'), 'w', encoding='utf-8') as f: + with open(pjoin(subdir, 'nb1.ipynb'), 'w', encoding='utf-8') as f: nb = new_notebook() write(nb, f, version=4) @@ -130,7 +128,7 @@ def test_create(self): self.assertIn('id', newsession) self.assertEqual(newsession['path'], 'foo/nb1.ipynb') self.assertEqual(newsession['type'], 'notebook') - self.assertEqual(resp.headers['Location'], self.url_prefix + 'api/sessions/{0}'.format(newsession['id'])) + self.assertEqual(resp.headers['Location'], f'{self.url_prefix}api/sessions/{newsession["id"]}') sessions = self.sess_api.list().json() self.assertEqual(sessions, [newsession]) @@ -174,7 +172,7 @@ def test_create_with_kernel_id(self): self.assertIn('id', newsession) self.assertEqual(newsession['path'], 'foo/nb1.ipynb') self.assertEqual(newsession['kernel']['id'], kernel['id']) - self.assertEqual(resp.headers['Location'], self.url_prefix + 'api/sessions/{0}'.format(newsession['id'])) + self.assertEqual(resp.headers['Location'], f'{self.url_prefix}api/sessions/{newsession["id"]}') sessions = self.sess_api.list().json() self.assertEqual(sessions, [newsession]) @@ -273,11 +271,11 @@ class AsyncSessionAPITest(SessionAPITest): def setup_class(cls): if not async_testing_enabled: # Can be removed once jupyter_client >= 6.1 is required. raise SkipTest("AsyncSessionAPITest tests skipped due to down-level jupyter_client!") - super(AsyncSessionAPITest, cls).setup_class() + super().setup_class() @classmethod def get_argv(cls): - argv = super(AsyncSessionAPITest, cls).get_argv() + argv = super().get_argv() # Before we extend the argv with the class, ensure that appropriate jupyter_client is available. # if not available, don't set kernel_manager_class, resulting in the repeat of sync-based tests. diff --git a/notebook/terminal/__init__.py b/notebook/terminal/__init__.py index 57e74c9bf4..401663702e 100644 --- a/notebook/terminal/__init__.py +++ b/notebook/terminal/__init__.py @@ -5,7 +5,7 @@ from ..utils import check_version if not check_version(terminado.__version__, '0.8.3'): - raise ImportError("terminado >= 0.8.3 required, found %s" % terminado.__version__) + raise ImportError(f"terminado >= 0.8.3 required, found {terminado.__version__}") from ipython_genutils.py3compat import which from notebook.utils import url_path_join as ujoin diff --git a/notebook/terminal/handlers.py b/notebook/terminal/handlers.py index 0e026d00ab..99a592d484 100644 --- a/notebook/terminal/handlers.py +++ b/notebook/terminal/handlers.py @@ -15,8 +15,12 @@ class TerminalHandler(IPythonHandler): """Render the terminal interface.""" @web.authenticated def get(self, term_name): - self.write(self.render_template('terminal.html', - ws_path="terminals/websocket/%s" % term_name)) + self.write( + self.render_template( + 'terminal.html', + ws_path=f"terminals/websocket/{term_name}", + ) + ) class NamedTerminalHandler(IPythonHandler): @@ -35,7 +39,7 @@ class NewTerminalHandler(IPythonHandler): def get(self, term_name): if term_name == 'new': raise web.HTTPError(400, "Terminal name 'new' is reserved.") - new_path = self.request.path.replace("new/{}".format(term_name), term_name) + new_path = self.request.path.replace(f"new/{term_name}", term_name) if term_name in self.terminal_manager.terminals: self.set_header('Location', new_path) self.set_status(302) @@ -50,7 +54,7 @@ class TermSocket(WebSocketMixin, IPythonHandler, terminado.TermSocket): def origin_check(self): """Terminado adds redundant origin_check - + Tornado already calls check_origin, so don't do anything here. """ return True diff --git a/notebook/terminal/terminalmanager.py b/notebook/terminal/terminalmanager.py index ed901310ec..5a4a29e29b 100644 --- a/notebook/terminal/terminalmanager.py +++ b/notebook/terminal/terminalmanager.py @@ -49,7 +49,7 @@ def create(self): def create_with_name(self, name): """Create a new terminal.""" if name in self.terminals: - raise web.HTTPError(409, "A terminal with name '{}' already exists.".format(name)) + raise web.HTTPError(409, f"A terminal with name '{name}' already exists.") term = self.get_terminal(name) return self._finish_create(name, term) @@ -110,7 +110,7 @@ def get_terminal_model(self, name): def _check_terminal(self, name): """Check a that terminal 'name' exists and raise 404 if not.""" if name not in self.terminals: - raise web.HTTPError(404, u'Terminal not found: %s' % name) + raise web.HTTPError(404, f'Terminal not found: {name}') def _initialize_culler(self): """Start culler if 'cull_inactive_timeout' is greater than zero. @@ -139,8 +139,9 @@ async def _cull_terminals(self): try: await self._cull_inactive_terminal(name) except Exception as e: - self.log.exception("The following exception was encountered while checking the " - "activity of terminal {}: {}".format(name, e)) + self.log.exception( + f"The following exception was encountered while checking the activity of terminal {name}: {e}" + ) async def _cull_inactive_terminal(self, name): try: diff --git a/notebook/terminal/tests/test_terminals_api.py b/notebook/terminal/tests/test_terminals_api.py index 9e0cba82da..2eb22174c9 100644 --- a/notebook/terminal/tests/test_terminals_api.py +++ b/notebook/terminal/tests/test_terminals_api.py @@ -9,7 +9,7 @@ from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error -class TerminalAPI(object): +class TerminalAPI: """Wrapper for terminal REST API requests""" def __init__(self, request, base_url, headers): self.request = request diff --git a/notebook/tests/conftest.py b/notebook/tests/conftest.py index b9aee32cdc..135e814673 100644 --- a/notebook/tests/conftest.py +++ b/notebook/tests/conftest.py @@ -1,9 +1,7 @@ - - def pytest_addoption(parser): parser.addoption('--integration_tests', action='store_true', dest="integration_tests", default=False, help="enable integration tests") def pytest_configure(config): if not config.option.integration_tests: - setattr(config.option, 'markexpr', 'not integration_tests') \ No newline at end of file + setattr(config.option, 'markexpr', 'not integration_tests') diff --git a/notebook/tests/launchnotebook.py b/notebook/tests/launchnotebook.py index 426ce43ffd..53686e8aac 100644 --- a/notebook/tests/launchnotebook.py +++ b/notebook/tests/launchnotebook.py @@ -69,12 +69,12 @@ def wait_until_dead(cls): cls.notebook_thread.join(timeout=MAX_WAITTIME) if cls.notebook_thread.is_alive(): raise TimeoutError("Undead notebook server") - + @classmethod def auth_headers(cls): headers = {} if cls.token: - headers['Authorization'] = 'token %s' % cls.token + headers['Authorization'] = f'token {cls.token}' return headers @staticmethod @@ -84,7 +84,7 @@ def fetch_url(url): @classmethod def request(cls, verb, path, **kwargs): """Send a request to my server - + with authentication and everything. """ headers = kwargs.setdefault('headers', {}) @@ -219,13 +219,13 @@ def cleanup_zmq(): @classmethod def base_url(cls): - return 'http://localhost:%i%s' % (cls.port, cls.url_prefix) + return f'http://localhost:{cls.port}{cls.url_prefix}' class UNIXSocketNotebookTestBase(NotebookTestBase): # Rely on `/tmp` to avoid any Linux socket length max buffer # issues. Key on PID for process-wise concurrency. - sock = '/tmp/.notebook.%i.sock' % os.getpid() + sock = f'/tmp/.notebook.{os.getpid()}.sock' @classmethod def get_bind_args(cls): @@ -233,7 +233,7 @@ def get_bind_args(cls): @classmethod def base_url(cls): - return '%s%s' % (urlencode_unix_socket(cls.sock), cls.url_prefix) + return f'{urlencode_unix_socket(cls.sock)}{cls.url_prefix}' @staticmethod def fetch_url(url): @@ -251,7 +251,7 @@ def assert_http_error(status, msg=None): except requests.HTTPError as e: real_status = e.response.status_code assert real_status == status, \ - "Expected status %d, got %d" % (status, real_status) + f"Expected status {status}, got {real_status}" if msg: assert msg in str(e), e else: diff --git a/notebook/tests/selenium/conftest.py b/notebook/tests/selenium/conftest.py index 64cdfa23bd..63dfff864d 100644 --- a/notebook/tests/selenium/conftest.py +++ b/notebook/tests/selenium/conftest.py @@ -39,8 +39,8 @@ def notebook_server(): info = {} with TemporaryDirectory() as td: nbdir = info['nbdir'] = pjoin(td, 'notebooks') - os.makedirs(pjoin(nbdir, u'sub ∂ir1', u'sub ∂ir 1a')) - os.makedirs(pjoin(nbdir, u'sub ∂ir2', u'sub ∂ir 1b')) + os.makedirs(pjoin(nbdir, 'sub ∂ir1', 'sub ∂ir 1a')) + os.makedirs(pjoin(nbdir, 'sub ∂ir2', 'sub ∂ir 1b')) info['extra_env'] = { 'JUPYTER_CONFIG_DIR': pjoin(td, 'jupyter_config'), @@ -60,7 +60,7 @@ def notebook_server(): print("command=", command) proc = info['popen'] = Popen(command, cwd=nbdir, env=env) info_file_path = pjoin(td, 'jupyter_runtime', - 'nbserver-%i.json' % proc.pid) + f'nbserver-{proc.pid:d}.json') info.update(_wait_for_server(proc, info_file_path)) print("Notebook server info:", info) @@ -91,10 +91,10 @@ def make_sauce_driver(): if capabilities['browserName'] == 'firefox': # Attempt to work around issue where browser loses authentication capabilities['version'] = '57.0' - hub_url = "%s:%s@localhost:4445" % (username, access_key) + hub_url = f"{username}:{access_key}@localhost:4445" print("Connecting remote driver on Sauce Labs") driver = Remote(desired_capabilities=capabilities, - command_executor="http://%s/wd/hub" % hub_url) + command_executor=f"http://{hub_url}/wd/hub") return driver diff --git a/notebook/tests/selenium/test_dashboard_nav.py b/notebook/tests/selenium/test_dashboard_nav.py index 8e09979025..f15a634578 100644 --- a/notebook/tests/selenium/test_dashboard_nav.py +++ b/notebook/tests/selenium/test_dashboard_nav.py @@ -1,21 +1,15 @@ import os -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC - from notebook.utils import url_path_join from notebook.tests.selenium.utils import wait_for_selector pjoin = os.path.join class PageError(Exception): - """Error for an action being incompatible with the current jupyter web page. - - """ + """Error for an action being incompatible with the current jupyter web page.""" def __init__(self, message): self.message = message - + def url_in_tree(browser, url=None): if url is None: @@ -26,7 +20,7 @@ def url_in_tree(browser, url=None): def get_list_items(browser): """Gets list items from a directory listing page - + Raises PageError if not in directory listing page (url has tree in it) """ if not url_in_tree(browser): @@ -42,9 +36,7 @@ def get_list_items(browser): } for a in browser.find_elements_by_class_name('item_link')] def only_dir_links(browser): - """Return only links that point at other directories in the tree - - """ + """Return only links that point at other directories in the tree""" items = get_list_items(browser) return [i for i in items if url_in_tree(browser, i['link']) and i['label'] != '..'] diff --git a/notebook/tests/selenium/test_deletecell.py b/notebook/tests/selenium/test_deletecell.py index 8253b9e63a..0e60adfaa6 100644 --- a/notebook/tests/selenium/test_deletecell.py +++ b/notebook/tests/selenium/test_deletecell.py @@ -1,6 +1,5 @@ - def cell_is_deletable(nb, index): - JS = 'return Jupyter.notebook.get_cell({}).is_deletable();'.format(index) + JS = f'return Jupyter.notebook.get_cell({index}).is_deletable();' return nb.browser.execute_script(JS) def remove_all_cells(notebook): @@ -20,11 +19,11 @@ def test_delete_cells(prefill_notebook): notebook.set_cell_metadata(0, 'deletable', 'false') notebook.set_cell_metadata(1, 'deletable', 0 - ) + ) assert not cell_is_deletable(notebook, 0) assert cell_is_deletable(notebook, 1) assert cell_is_deletable(notebook, 2) - + # Try to delete cell a (should not be deleted) notebook.delete_cell(0) assert notebook.get_cells_contents() == [a, b, c] diff --git a/notebook/tests/selenium/test_display_image.py b/notebook/tests/selenium/test_display_image.py index 0ee9f0c800..4e3adfd598 100644 --- a/notebook/tests/selenium/test_display_image.py +++ b/notebook/tests/selenium/test_display_image.py @@ -27,9 +27,9 @@ def validate_img(notebook, cell_index, image_fmt, retina): b64data = b64_image_data[image_fmt] commands = [ - 'b64data = %s' % b64data, + f'b64data = {b64data}', 'data = base64.decodebytes(b64data)', - 'display(Image(data, retina=%s))' % retina + f'display(Image(data, retina={retina}))' ] notebook.append("\n".join(commands)) notebook.execute_cell(cell_index) @@ -40,7 +40,7 @@ def validate_img(notebook, cell_index, image_fmt, retina): src = img_element.get_attribute("src") prefix = src.split(',')[0] - expected_prefix = "data:%s;base64" % image_fmt + expected_prefix = f"data:{image_fmt};base64" assert prefix == expected_prefix expected_size = 1 if retina else 2 diff --git a/notebook/tests/selenium/test_display_isolation.py b/notebook/tests/selenium/test_display_isolation.py index 461f31352b..51ca082bcd 100644 --- a/notebook/tests/selenium/test_display_isolation.py +++ b/notebook/tests/selenium/test_display_isolation.py @@ -28,18 +28,16 @@ def isolated_html(notebook): red = 'rgb(255, 0, 0)' blue = 'rgb(0, 0, 255)' test_str = "
Should turn red from non-isolation
" - notebook.add_and_execute_cell(content="display(HTML(%r))" % test_str) + notebook.add_and_execute_cell(content=f"display(HTML({test_str!r}))") non_isolated = ( - "" % red + - "
Should be red
") - display_ni = "display(HTML(%r), metadata={'isolated':False})" % ( - non_isolated) + f"" + f"
Should be red
") + display_ni = f"display(HTML({non_isolated!r}), metadata={{'isolated':False}})" notebook.add_and_execute_cell(content=display_ni) isolated = ( - "" % blue + - "
Should be blue
") - display_i = "display(HTML(%r), metadata={'isolated':True})" % ( - isolated) + f"" + f"
Should be blue
") + display_i = f"display(HTML({isolated!r}), metadata={{'isolated':True}})" notebook.add_and_execute_cell(content=display_i) iframe = wait_for_tag(notebook.browser, "iframe", single=True) @@ -69,7 +67,7 @@ def isolated_svg(notebook): """ yellow = "rgb(255, 255, 0)" black = "rgb(0, 0, 0)" - svg_1_str = """s1 = ''''''""" % yellow + svg_1_str = f"""s1 = ''''''""" svg_2_str = """s2 = ''''''""" notebook.add_and_execute_cell(content=svg_1_str) diff --git a/notebook/tests/selenium/test_kernel_menu.py b/notebook/tests/selenium/test_kernel_menu.py index 678f39be8e..b68b5b3db3 100644 --- a/notebook/tests/selenium/test_kernel_menu.py +++ b/notebook/tests/selenium/test_kernel_menu.py @@ -1,4 +1,3 @@ -from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait @@ -56,5 +55,5 @@ def test_menu_items(notebook): wait_for_selector(browser, menu_item, visible=True, single=True).click() WebDriverWait(browser, 10).until( lambda b: notebook.is_kernel_running(), - message="Restart (%r) after shutdown did not start kernel" % menu_item + message=f"Restart ({menu_item!r}) after shutdown did not start kernel" ) diff --git a/notebook/tests/selenium/test_merge_cells.py b/notebook/tests/selenium/test_merge_cells.py index 23c2b87e85..0fb4dd5662 100644 --- a/notebook/tests/selenium/test_merge_cells.py +++ b/notebook/tests/selenium/test_merge_cells.py @@ -19,18 +19,18 @@ def test_merge_cells(prefill_notebook): # Focus on the second cell and merge it with the cell above notebook.focus_cell(1) notebook.browser.execute_script("Jupyter.notebook.merge_cell_above();") - merged_a_b = "%s\n\n%s" % (a, b) + merged_a_b = f"{a}\n\n{b}" assert notebook.get_cells_contents() == [merged_a_b, c, d, e, f] # Focus on the second cell and merge it with the cell below notebook.focus_cell(1) notebook.browser.execute_script("Jupyter.notebook.merge_cell_below();") - merged_c_d = "%s\n\n%s" % (c, d) + merged_c_d = f"{c}\n\n{d}" assert notebook.get_cells_contents() == [merged_a_b, merged_c_d, e, f] # Merge everything down to a single cell with selected cells notebook.select_cell_range(0,3) notebook.browser.execute_script("Jupyter.notebook.merge_selected_cells();") - merged_all = "%s\n\n%s\n\n%s\n\n%s" % (merged_a_b, merged_c_d, e, f) + merged_all = f"{merged_a_b}\n\n{merged_c_d}\n\n{e}\n\n{f}" assert notebook.get_cells_contents() == [merged_all] diff --git a/notebook/tests/selenium/test_multiselect_toggle.py b/notebook/tests/selenium/test_multiselect_toggle.py index 14f51d0259..372d83b275 100644 --- a/notebook/tests/selenium/test_multiselect_toggle.py +++ b/notebook/tests/selenium/test_multiselect_toggle.py @@ -1,4 +1,3 @@ - INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")'] def test_multiselect_toggle(prefill_notebook): notebook = prefill_notebook(INITIAL_CELLS) @@ -24,7 +23,7 @@ def select_cells(): cell_output_states = notebook.browser.execute_script( "return Jupyter.notebook.get_cells().map(c => c.collapsed)") assert cell_output_states == [False] * 3, "ensure that all cells are not collapsed" - + # Test that cells, which start off not scrolled are scrolled after # calling the multiselected scroll toggle. select_cells() diff --git a/notebook/tests/selenium/test_prompt_numbers.py b/notebook/tests/selenium/test_prompt_numbers.py index 42e27775ec..38872b8550 100755 --- a/notebook/tests/selenium/test_prompt_numbers.py +++ b/notebook/tests/selenium/test_prompt_numbers.py @@ -1,4 +1,3 @@ - def test_prompt_numbers(prefill_notebook): notebook = prefill_notebook(['print("a")']) diff --git a/notebook/tests/selenium/test_save.py b/notebook/tests/selenium/test_save.py index 3ed7721f31..d566866b06 100644 --- a/notebook/tests/selenium/test_save.py +++ b/notebook/tests/selenium/test_save.py @@ -58,8 +58,7 @@ def test_save(notebook): break hrefs_nonmatch.append(href) else: - raise AssertionError("{!r} not found in {!r}" - .format(escaped_name, hrefs_nonmatch)) + raise AssertionError(f"{escaped_name!r} not found in {hrefs_nonmatch!r}") current_name = notebook.browser.execute_script("return Jupyter.notebook.notebook_name") assert current_name == nbname diff --git a/notebook/tests/selenium/test_save_as_notebook.py b/notebook/tests/selenium/test_save_as_notebook.py index 567087d438..781410ce83 100644 --- a/notebook/tests/selenium/test_save_as_notebook.py +++ b/notebook/tests/selenium/test_save_as_notebook.py @@ -19,8 +19,8 @@ def get_notebook_name(nb): return nb.browser.execute_script(JS) def set_notebook_name(nb, name): - JS = 'Jupyter.notebook.rename("{}")'.format(name) - nb.browser.execute_script(JS) + JS = f'Jupyter.notebook.rename("{name}")' + nb.browser.execute_script(JS) def test_save_notebook_as(notebook): # Set a name for comparison later diff --git a/notebook/tests/selenium/test_save_readonly_as.py b/notebook/tests/selenium/test_save_readonly_as.py index 12e7b720c7..94c066753b 100644 --- a/notebook/tests/selenium/test_save_readonly_as.py +++ b/notebook/tests/selenium/test_save_readonly_as.py @@ -1,4 +1,4 @@ -from notebook.tests.selenium.utils import wait_for_selector, Notebook +from notebook.tests.selenium.utils import wait_for_selector from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait diff --git a/notebook/tests/selenium/utils.py b/notebook/tests/selenium/utils.py index 4407fce39d..d828611cfb 100644 --- a/notebook/tests/selenium/utils.py +++ b/notebook/tests/selenium/utils.py @@ -92,29 +92,29 @@ def multiple_found(driver): class CellTypeError(ValueError): - + def __init__(self, message=""): self.message = message class Notebook: - + def __init__(self, browser): self.browser = browser self._wait_for_start() self.disable_autosave_and_onbeforeunload() - + def __len__(self): return len(self.cells) - + def __getitem__(self, key): return self.cells[key] - + def __setitem__(self, key, item): if isinstance(key, int): self.edit_cell(index=key, content=item, render=False) # TODO: re-add slicing support, handle general python slicing behaviour - # includes: overwriting the entire self.cells object if you do + # includes: overwriting the entire self.cells object if you do # self[:] = [] # elif isinstance(key, slice): # indices = (self.index(cell) for cell in self[key]) @@ -138,20 +138,20 @@ def body(self): @property def cells(self): """Gets all cells once they are visible. - + """ return self.browser.find_elements_by_class_name("cell") - + @property def current_index(self): return self.index(self.current_cell) - + def index(self, cell): return self.cells.index(cell) def disable_autosave_and_onbeforeunload(self): """Disable request to save before closing window and autosave. - + This is most easily done by using js directly. """ self.browser.execute_script("window.onbeforeunload = null;") @@ -159,7 +159,7 @@ def disable_autosave_and_onbeforeunload(self): def to_command_mode(self): """Changes us into command mode on currently focused cell - + """ self.body.send_keys(Keys.ESCAPE) self.browser.execute_script("return Jupyter.notebook.handle_command_mode(" @@ -171,7 +171,7 @@ def focus_cell(self, index=0): cell.click() self.to_command_mode() self.current_cell = cell - + def select_cell_range(self, initial_index=0, final_index=0): self.focus_cell(initial_index) self.to_command_mode() @@ -199,9 +199,8 @@ def convert_cell_type(self, index=0, cell_type="code"): elif cell_type == "code": self.current_cell.send_keys("y") else: - raise CellTypeError(("{} is not a valid cell type," - "use 'code', 'markdown', or 'raw'").format(cell_type)) - + raise CellTypeError(f"{cell_type} is not a valid cell type,use 'code', 'markdown', or 'raw'") + self.wait_for_stale_cell(cell) self.focus_cell(index) return self.current_cell @@ -209,7 +208,7 @@ def convert_cell_type(self, index=0, cell_type="code"): def wait_for_stale_cell(self, cell): """ This is needed to switch a cell's mode and refocus it, or to render it. - Warning: there is currently no way to do this when changing between + Warning: there is currently no way to do this when changing between markdown and raw cells. """ wait = WebDriverWait(self.browser, 10) @@ -234,15 +233,15 @@ def wait_for_cell_output(self, index=0, timeout=10): ) def set_cell_metadata(self, index, key, value): - JS = 'Jupyter.notebook.get_cell({}).metadata.{} = {}'.format(index, key, value) + JS = f'Jupyter.notebook.get_cell({index}).metadata.{key} = {value}' return self.browser.execute_script(JS) def get_cell_type(self, index=0): - JS = 'return Jupyter.notebook.get_cell({}).cell_type'.format(index) + JS = f'return Jupyter.notebook.get_cell({index}).cell_type' return self.browser.execute_script(JS) - + def set_cell_input_prompt(self, index, prmpt_val): - JS = 'Jupyter.notebook.get_cell({}).set_input_prompt({})'.format(index, prmpt_val) + JS = f'Jupyter.notebook.get_cell({index}).set_input_prompt({prmpt_val})' self.browser.execute_script(JS) def edit_cell(self, cell=None, index=0, content="", render=False): @@ -267,7 +266,7 @@ def edit_cell(self, cell=None, index=0, content="", render=False): def execute_cell(self, cell_or_index=None): if isinstance(cell_or_index, int): index = cell_or_index - elif isinstance(cell_or_index, WebElement): + elif isinstance(cell_or_index, WebElement): index = self.index(cell_or_index) else: raise TypeError("execute_cell only accepts a WebElement or an int") @@ -295,18 +294,18 @@ def delete_cell(self, index): def add_markdown_cell(self, index=-1, content="", render=True): self.add_cell(index, cell_type="markdown") self.edit_cell(index=index, content=content, render=render) - + def append(self, *values, cell_type="code"): for i, value in enumerate(values): if isinstance(value, str): self.add_cell(cell_type=cell_type, content=value) else: - raise TypeError("Don't know how to add cell from %r" % value) - + raise TypeError(f"Don't know how to add cell from {value!r}") + def extend(self, values): self.append(*values) - + def run_all(self): for cell in self: self.execute_cell(cell) @@ -320,7 +319,7 @@ def is_kernel_running(self): ) def clear_cell_output(self, index): - JS = 'Jupyter.notebook.clear_output({})'.format(index) + JS = f'Jupyter.notebook.clear_output({index})' self.browser.execute_script(JS) @classmethod @@ -336,24 +335,24 @@ def select_kernel(browser, kernel_name='kernel-python3'): wait = WebDriverWait(browser, 10) new_button = wait.until(EC.element_to_be_clickable((By.ID, "new-dropdown-button"))) new_button.click() - kernel_selector = '#{} a'.format(kernel_name) + kernel_selector = f'#{kernel_name} a' kernel = wait_for_selector(browser, kernel_selector, single=True) kernel.click() @contextmanager def new_window(browser): - """Contextmanager for switching to & waiting for a window created. - - This context manager gives you the ability to create a new window inside + """Contextmanager for switching to & waiting for a window created. + + This context manager gives you the ability to create a new window inside the created context and it will switch you to that new window. - + Usage example: - + from notebook.tests.selenium.utils import new_window, Notebook - + ⋮ # something that creates a browser object - + with new_window(browser): select_kernel(browser, kernel_name=kernel_name) nb = Notebook(browser) @@ -400,7 +399,7 @@ def trigger_keystrokes(browser, *keys): browser.send_keys(getattr(Keys, keys[0].upper(), keys[0])) def validate_dualmode_state(notebook, mode, index): - '''Validate the entire dual mode state of the notebook. + '''Validate the entire dual mode state of the notebook. Checks if the specified cell is selected, and the mode and keyboard mode are the same. Depending on the mode given: Command: Checks that no cells are in focus or in edit mode. @@ -462,7 +461,7 @@ def is_focused_on(index): assert is_focused_on(None) #no focused cells assert is_only_cell_edit(None) #no cells in edit mode - + elif mode == 'edit': assert is_focused_on(index) #The specified cell is focused diff --git a/notebook/tests/test_files.py b/notebook/tests/test_files.py index b711945756..c7abbc3eba 100644 --- a/notebook/tests/test_files.py +++ b/notebook/tests/test_files.py @@ -1,12 +1,9 @@ """Test the /files/ handler.""" -import io import os -from unicodedata import normalize pjoin = os.path.join -import requests import json from nbformat import write @@ -16,21 +13,20 @@ from notebook.utils import url_path_join from .launchnotebook import NotebookTestBase -from ipython_genutils import py3compat class FilesTest(NotebookTestBase): def test_hidden_files(self): not_hidden = [ - u'å b', - u'å b/ç. d', + 'å b', + 'å b/ç. d', ] hidden = [ - u'.å b', - u'å b/.ç d', + '.å b', + 'å b/.ç d', ] dirs = not_hidden + hidden - + nbdir = self.notebook_dir for d in dirs: path = pjoin(nbdir, d.replace('/', os.sep)) @@ -82,23 +78,23 @@ def test_contents_manager(self): nb = new_notebook( cells=[ - new_markdown_cell(u'Created by test ³'), + new_markdown_cell('Created by test ³'), new_code_cell("print(2*6)", outputs=[ new_output("stream", text="12"), ]) ] ) - with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w', + with open(pjoin(nbdir, 'testnb.ipynb'), 'w', encoding='utf-8') as f: write(nb, f, version=4) - with io.open(pjoin(nbdir, 'test.bin'), 'wb') as f: + with open(pjoin(nbdir, 'test.bin'), 'wb') as f: f.write(b'\xff' + os.urandom(5)) f.close() - with io.open(pjoin(nbdir, 'test.txt'), 'w') as f: - f.write(u'foobar') + with open(pjoin(nbdir, 'test.txt'), 'w') as f: + f.write('foobar') f.close() r = self.request('GET', 'files/testnb.ipynb') @@ -116,14 +112,14 @@ def test_contents_manager(self): self.assertEqual(r.status_code, 200) self.assertEqual(r.headers['content-type'], 'text/plain; charset=UTF-8') self.assertEqual(r.text, 'foobar') - + def test_download(self): nbdir = self.notebook_dir - + text = 'hello' with open(pjoin(nbdir, 'test.txt'), 'w') as f: f.write(text) - + r = self.request('GET', 'files/test.txt') disposition = r.headers.get('Content-Disposition', '') self.assertNotIn('attachment', disposition) @@ -132,24 +128,24 @@ def test_download(self): disposition = r.headers.get('Content-Disposition', '') self.assertIn('attachment', disposition) self.assertIn("filename*=utf-8''test.txt", disposition) - + def test_view_html(self): nbdir = self.notebook_dir - + html = '
Test test
' with open(pjoin(nbdir, 'test.html'), 'w') as f: f.write(html) - + r = self.request('GET', 'view/test.html') self.assertEqual(r.status_code, 200) def test_old_files_redirect(self): """pre-2.0 'files/' prefixed links are properly redirected""" nbdir = self.notebook_dir - + os.mkdir(pjoin(nbdir, 'files')) os.makedirs(pjoin(nbdir, 'sub', 'files')) - + for prefix in ('', 'sub'): with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f: f.write(prefix + '/files/f1') diff --git a/notebook/tests/test_gateway.py b/notebook/tests/test_gateway.py index f84ba06369..fc198177ac 100644 --- a/notebook/tests/test_gateway.py +++ b/notebook/tests/test_gateway.py @@ -6,7 +6,6 @@ from io import StringIO from unittest.mock import patch -from tornado import gen from tornado.web import HTTPError from tornado.httpclient import HTTPRequest, HTTPResponse @@ -62,7 +61,7 @@ async def mock_gateway_request(url, **kwargs): response = await maybe_future(HTTPResponse(request, 200, buffer=response_buf)) return response else: - raise HTTPError(404, message='Kernelspec does not exist: %s' % requested_kernelspec) + raise HTTPError(404, message=f'Kernelspec does not exist: {requested_kernelspec}') # Create kernel if endpoint.endswith('/api/kernels') and method == 'POST': @@ -96,16 +95,16 @@ async def mock_gateway_request(url, **kwargs): response = await maybe_future(HTTPResponse(request, 204)) return response else: - raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id) + raise HTTPError(404, message=f'Kernel does not exist: {requested_kernel_id}') elif action == 'restart': if requested_kernel_id in running_kernels: response_buf = StringIO(json.dumps(running_kernels.get(requested_kernel_id))) response = await maybe_future(HTTPResponse(request, 204, buffer=response_buf)) return response else: - raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id) + raise HTTPError(404, message=f'Kernel does not exist: {requested_kernel_id}') else: - raise HTTPError(404, message='Bad action detected: %s' % action) + raise HTTPError(404, message=f'Bad action detected: {action}') # Shutdown existing kernel if endpoint.rfind('/api/kernels/') >= 0 and method == 'DELETE': @@ -122,7 +121,7 @@ async def mock_gateway_request(url, **kwargs): response = await maybe_future(HTTPResponse(request, 200, buffer=response_buf)) return response else: - raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id) + raise HTTPError(404, message=f'Kernel does not exist: {requested_kernel_id}') mocked_gateway = patch('notebook.gateway.managers.gateway_request', mock_gateway_request) @@ -136,23 +135,23 @@ class TestGateway(NotebookTestBase): @classmethod def setup_class(cls): GatewayClient.clear_instance() - super(TestGateway, cls).setup_class() + super().setup_class() @classmethod def teardown_class(cls): GatewayClient.clear_instance() - super(TestGateway, cls).teardown_class() + super().teardown_class() @classmethod def get_patch_env(cls): - test_env = super(TestGateway, cls).get_patch_env() + test_env = super().get_patch_env() test_env.update({'JUPYTER_GATEWAY_URL': TestGateway.mock_gateway_url, 'JUPYTER_GATEWAY_CONNECT_TIMEOUT': '44.4'}) return test_env @classmethod def get_argv(cls): - argv = super(TestGateway, cls).get_argv() + argv = super().get_argv() argv.extend(['--GatewayClient.request_timeout=96.0', '--GatewayClient.http_user=' + TestGateway.mock_http_user]) return argv diff --git a/notebook/tests/test_nbextensions.py b/notebook/tests/test_nbextensions.py index 3d9549658a..655b2284e9 100644 --- a/notebook/tests/test_nbextensions.py +++ b/notebook/tests/test_nbextensions.py @@ -69,9 +69,9 @@ def cleanup_tempdirs(): self.src = self.tempdir() self.files = files = [ - pjoin(u'ƒile'), - pjoin(u'∂ir', u'ƒile1'), - pjoin(u'∂ir', u'∂ir2', u'ƒile2'), + pjoin('ƒile'), + pjoin('∂ir', 'ƒile1'), + pjoin('∂ir', '∂ir2', 'ƒile2'), ] for file_name in files: fullpath = os.path.join(self.src, file_name) @@ -107,15 +107,15 @@ def cleanup_tempdirs(): def assert_dir_exists(self, path): if not os.path.exists(path): do_exist = os.listdir(os.path.dirname(path)) - self.fail(u"%s should exist (found %s)" % (path, do_exist)) + self.fail(f"{path} should exist (found {do_exist})") def assert_not_dir_exists(self, path): if os.path.exists(path): - self.fail(u"%s should not exist" % path) + self.fail(f"{path} should not exist") def assert_installed(self, relative_path, user=False): if user: - nbext = pjoin(self.data_dir, u'nbextensions') + nbext = pjoin(self.data_dir, 'nbextensions') else: nbext = self.system_nbext self.assert_dir_exists( @@ -124,7 +124,7 @@ def assert_installed(self, relative_path, user=False): def assert_not_installed(self, relative_path, user=False): if user: - nbext = pjoin(self.data_dir, u'nbextensions') + nbext = pjoin(self.data_dir, 'nbextensions') else: nbext = self.system_nbext self.assert_not_dir_exists( @@ -150,17 +150,17 @@ def test_create_nbextensions_user(self): with TemporaryDirectory() as td: install_nbextension(self.src, user=True) self.assert_installed( - pjoin(basename(self.src), u'ƒile'), + pjoin(basename(self.src), 'ƒile'), user=True ) def test_create_nbextensions_system(self): with TemporaryDirectory() as td: - self.system_nbext = pjoin(td, u'nbextensions') + self.system_nbext = pjoin(td, 'nbextensions') with patch.object(nbextensions, 'SYSTEM_JUPYTER_PATH', [td]): install_nbextension(self.src, user=False) self.assert_installed( - pjoin(basename(self.src), u'ƒile'), + pjoin(basename(self.src), 'ƒile'), user=False ) @@ -170,28 +170,28 @@ def test_single_file(self): self.assert_installed(file_name) def test_single_dir(self): - d = u'∂ir' + d = '∂ir' install_nbextension(pjoin(self.src, d)) self.assert_installed(self.files[-1]) def test_single_dir_trailing_slash(self): - d = u'∂ir/' + d = '∂ir/' install_nbextension(pjoin(self.src, d)) self.assert_installed(self.files[-1]) if os.name == 'nt': - d = u'∂ir\\' + d = '∂ir\\' install_nbextension(pjoin(self.src, d)) self.assert_installed(self.files[-1]) def test_destination_file(self): file_name = self.files[0] - install_nbextension(pjoin(self.src, file_name), destination = u'ƒiledest') - self.assert_installed(u'ƒiledest') + install_nbextension(pjoin(self.src, file_name), destination = 'ƒiledest') + self.assert_installed('ƒiledest') def test_destination_dir(self): - d = u'∂ir' - install_nbextension(pjoin(self.src, d), destination = u'ƒiledest2') - self.assert_installed(pjoin(u'ƒiledest2', u'∂ir2', u'ƒile2')) + d = '∂ir' + install_nbextension(pjoin(self.src, d), destination = 'ƒiledest2') + self.assert_installed(pjoin('ƒiledest2', '∂ir2', 'ƒile2')) def test_install_nbextension(self): with self.assertRaises(TypeError): @@ -199,7 +199,7 @@ def test_install_nbextension(self): def test_overwrite_file(self): with TemporaryDirectory() as d: - fname = u'ƒ.js' + fname = 'ƒ.js' src = pjoin(d, fname) with open(src, 'w') as f: f.write('first') @@ -216,12 +216,12 @@ def test_overwrite_file(self): def test_overwrite_dir(self): with TemporaryDirectory() as src: base = basename(src) - fname = u'ƒ.js' + fname = 'ƒ.js' touch(pjoin(src, fname)) install_nbextension(src) self.assert_installed(pjoin(base, fname)) os.remove(pjoin(src, fname)) - fname2 = u'∂.js' + fname2 = '∂.js' touch(pjoin(src, fname2)) install_nbextension(src, overwrite=True) self.assert_installed(pjoin(base, fname2)) @@ -229,7 +229,7 @@ def test_overwrite_dir(self): def test_update_file(self): with TemporaryDirectory() as d: - fname = u'ƒ.js' + fname = 'ƒ.js' src = pjoin(d, fname) with open(src, 'w') as f: f.write('first') @@ -247,7 +247,7 @@ def test_update_file(self): def test_skip_old_file(self): with TemporaryDirectory() as d: - fname = u'ƒ.js' + fname = 'ƒ.js' src = pjoin(d, fname) mtime = touch(src) install_nbextension(src) @@ -287,11 +287,11 @@ def _add_file(f, fname, buf): for i,ext in enumerate((".tar.gz", ".tgz", ".tar.bz2")): path = pjoin(self.src, "myjsext" + ext) with tarfile.open(path, 'w') as f: - _add_file(f, "b%i.js" % i, b"b();") - _add_file(f, "foo/b%i.js" % i, b"foo();") + _add_file(f, f"b{i}.js", b"b();") + _add_file(f, f"foo/b{i}.js", b"foo();") install_nbextension(path) - self.assert_installed("b%i.js" % i) - self.assert_installed(pjoin("foo", "b%i.js" % i)) + self.assert_installed(f"b{i}.js") + self.assert_installed(pjoin("foo", f"b{i}.js")) def test_install_url(self): def fake_urlretrieve(url, dest): @@ -311,7 +311,7 @@ def fake_urlretrieve(url, dest): def test_check_nbextension(self): with TemporaryDirectory() as d: - f = u'ƒ.js' + f = 'ƒ.js' src = pjoin(d, f) touch(src) install_nbextension(src, user=True) @@ -323,7 +323,7 @@ def test_check_nbextension(self): @pytest.mark.skipif(sys.platform == "win32", reason="do not run on windows") def test_install_symlink(self): with TemporaryDirectory() as d: - f = u'ƒ.js' + f = 'ƒ.js' src = pjoin(d, f) touch(src) install_nbextension(src, symlink=True) @@ -335,8 +335,8 @@ def test_install_symlink(self): @pytest.mark.skipif(sys.platform == "win32", reason="do not run on windows") def test_overwrite_broken_symlink(self): with TemporaryDirectory() as d: - f = u'ƒ.js' - f2 = u'ƒ2.js' + f = 'ƒ.js' + f2 = 'ƒ2.js' src = pjoin(d, f) src2 = pjoin(d, f2) touch(src) @@ -351,8 +351,8 @@ def test_overwrite_broken_symlink(self): @pytest.mark.skipif(sys.platform == "win32", reason="do not run on windows") def test_install_symlink_destination(self): with TemporaryDirectory() as d: - f = u'ƒ.js' - flink = u'ƒlink.js' + f = 'ƒ.js' + flink = 'ƒlink.js' src = pjoin(d, f) touch(src) install_nbextension(src, symlink=True, destination=flink) @@ -367,7 +367,7 @@ def test_install_symlink_bad(self): install_nbextension("http://example.com/foo.js", symlink=True) with TemporaryDirectory() as d: - zf = u'ƒ.zip' + zf = 'ƒ.zip' zsrc = pjoin(d, zf) with zipfile.ZipFile(zsrc, 'w') as z: z.writestr("a.js", b"b();") @@ -377,7 +377,7 @@ def test_install_symlink_bad(self): def test_install_destination_bad(self): with TemporaryDirectory() as d: - zf = u'ƒ.zip' + zf = 'ƒ.zip' zsrc = pjoin(d, zf) with zipfile.ZipFile(zsrc, 'w') as z: z.writestr("a.js", b"b();") @@ -387,24 +387,24 @@ def test_install_destination_bad(self): def test_nbextension_enable(self): with TemporaryDirectory() as d: - f = u'ƒ.js' + f = 'ƒ.js' src = pjoin(d, f) touch(src) install_nbextension(src, user=True) - enable_nbextension(section='notebook', require=u'ƒ') + enable_nbextension(section='notebook', require='ƒ') config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig') cm = BaseJSONConfigManager(config_dir=config_dir) - enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False) + enabled = cm.get('notebook').get('load_extensions', {}).get('ƒ', False) assert enabled def test_nbextension_disable(self): self.test_nbextension_enable() - disable_nbextension(section='notebook', require=u'ƒ') + disable_nbextension(section='notebook', require='ƒ') config_dir = os.path.join(_get_config_dir(user=True), 'nbconfig') cm = BaseJSONConfigManager(config_dir=config_dir) - enabled = cm.get('notebook').get('load_extensions', {}).get(u'ƒ', False) + enabled = cm.get('notebook').get('load_extensions', {}).get('ƒ', False) assert not enabled diff --git a/notebook/tests/test_notebookapp.py b/notebook/tests/test_notebookapp.py index d48501b935..8a87d58c73 100644 --- a/notebook/tests/test_notebookapp.py +++ b/notebook/tests/test_notebookapp.py @@ -4,8 +4,6 @@ import logging import os import re -import signal -from subprocess import Popen, PIPE, STDOUT import sys from tempfile import NamedTemporaryFile @@ -159,7 +157,7 @@ def list_running_servers(runtime_dir): 'secure': False, 'token': '', 'password': False, - 'url': 'http://localhost:%i' % port, + 'url': f'http://localhost:{port}', } mock_servers = patch('notebook.notebookapp.list_running_servers', list_running_servers) @@ -211,7 +209,7 @@ class NotebookAppJSONLoggingTests(NotebookTestBase): """Tests for when json logging is enabled.""" @classmethod def setup_class(cls): - super(NotebookAppJSONLoggingTests, cls).setup_class() + super().setup_class() try: import json_logging cls.json_logging_available = True @@ -220,7 +218,7 @@ def setup_class(cls): @classmethod def get_patch_env(cls): - test_env = super(NotebookAppJSONLoggingTests, cls).get_patch_env() + test_env = super().get_patch_env() test_env.update({'JUPYTER_ENABLE_JSON_LOGGING': 'true'}) return test_env diff --git a/notebook/tests/test_notebookapp_integration.py b/notebook/tests/test_notebookapp_integration.py index 328fab05bc..e03ea0203d 100644 --- a/notebook/tests/test_notebookapp_integration.py +++ b/notebook/tests/test_notebookapp_integration.py @@ -1,5 +1,4 @@ import os -import pytest import stat import subprocess import sys @@ -22,7 +21,7 @@ def test_shutdown_sock_server_integration(): encoded_sock_path = urlencode_unix_socket_path(sock) p = subprocess.Popen( - ['jupyter-notebook', '--sock=%s' % sock, '--sock-mode=0700'], + ['jupyter-notebook', f'--sock={sock}', '--sock-mode=0700'], stdout=subprocess.PIPE, stderr=subprocess.PIPE ) @@ -111,12 +110,12 @@ def test_stop_multi_integration(): # Unix socket. sock = UNIXSocketNotebookTestBase.sock p2 = subprocess.Popen( - ['jupyter-notebook', '--sock=%s' % sock] + ['jupyter-notebook', f'--sock={sock}'] ) # Specified port p3 = subprocess.Popen( - ['jupyter-notebook', '--no-browser', '--port=%s' % TEST_PORT] + ['jupyter-notebook', '--no-browser', f'--port={TEST_PORT}'] ) time.sleep(3) @@ -146,12 +145,12 @@ def test_stop_multi_integration(): def test_launch_socket_collision(): """Tests UNIX socket in-use detection for lifecycle correctness.""" sock = UNIXSocketNotebookTestBase.sock - check_msg = 'socket %s is already in use' % sock + check_msg = f'socket {sock} is already in use' _ensure_stopped() # Start a server. - cmd = ['jupyter-notebook', '--sock=%s' % sock] + cmd = ['jupyter-notebook', f'--sock={sock}'] p1 = subprocess.Popen(cmd) time.sleep(3) @@ -161,7 +160,7 @@ def test_launch_socket_collision(): except subprocess.CalledProcessError as e: assert check_msg in e.output.decode() else: - raise AssertionError('expected error, instead got %s' % e.output.decode()) + raise AssertionError(f'expected error, instead got {e.output.decode()}') # Stop the background server, ensure it's stopped and wait on the process to exit. subprocess.check_call(['jupyter-notebook', 'stop', sock]) diff --git a/notebook/tests/test_paths.py b/notebook/tests/test_paths.py index 33f44afe4d..fc8dc14847 100644 --- a/notebook/tests/test_paths.py +++ b/notebook/tests/test_paths.py @@ -1,4 +1,3 @@ - import re from notebook.base.handlers import path_regex @@ -6,7 +5,7 @@ from .launchnotebook import NotebookTestBase # build regexps that tornado uses: -path_pat = re.compile('^' + '/x%s' % path_regex + '$') +path_pat = re.compile(f'^/x{path_regex}$') def test_path_regex(): diff --git a/notebook/tests/test_serverextensions.py b/notebook/tests/test_serverextensions.py index 898647926b..b4a94806bc 100644 --- a/notebook/tests/test_serverextensions.py +++ b/notebook/tests/test_serverextensions.py @@ -1,4 +1,3 @@ -import imp import os import site import sys @@ -13,7 +12,7 @@ from jupyter_core import paths from notebook.serverextensions import toggle_serverextension_python -from notebook import nbextensions, serverextensions, extensions +from notebook import nbextensions, extensions from notebook.notebookapp import NotebookApp from notebook.nbextensions import _get_config_dir @@ -31,7 +30,7 @@ def test_help_output(): outer_file = __file__ -class MockExtensionModule(object): +class MockExtensionModule: __file__ = outer_file @staticmethod @@ -41,13 +40,13 @@ def _jupyter_server_extension_paths(): }] loaded = False - + def load_jupyter_server_extension(self, app): self.loaded = True class MockEnvTestCase(TestCase): - + def tempdir(self): td = TemporaryDirectory() self.tempdirs.append(td) @@ -64,7 +63,7 @@ def setUp(self): self.system_config_dir = os.path.join(self.test_dir, 'system_config') self.system_path = [self.system_data_dir] self.system_config_path = [self.system_config_dir] - + self.patches = [] p = patch.dict('os.environ', { 'JUPYTER_CONFIG_DIR': self.config_dir, @@ -97,7 +96,7 @@ def setUp(self): self.assertEqual(paths.jupyter_config_path(), [self.config_dir] + self.system_config_path) self.assertEqual(extensions._get_config_dir(user=False), self.system_config_dir) self.assertEqual(paths.jupyter_path(), [self.data_dir] + self.system_path) - + def tearDown(self): for modulename in self._mock_extensions: sys.modules.pop(modulename) diff --git a/notebook/tests/test_utils.py b/notebook/tests/test_utils.py index 51f0e8accc..b6c386a2bc 100644 --- a/notebook/tests/test_utils.py +++ b/notebook/tests/test_utils.py @@ -84,7 +84,7 @@ def test_is_hidden(): def test_is_hidden_win32(): with TemporaryDirectory() as root: root = cast_unicode(root) - subdir1 = os.path.join(root, u'subdir') + subdir1 = os.path.join(root, 'subdir') os.makedirs(subdir1) assert not is_hidden(subdir1, root) r = ctypes.windll.kernel32.SetFileAttributesW(subdir1, 0x02) diff --git a/notebook/traittypes.py b/notebook/traittypes.py index 226657c1f4..53bb125369 100644 --- a/notebook/traittypes.py +++ b/notebook/traittypes.py @@ -75,7 +75,7 @@ class name where an object was defined. if article == "the" or (article is None and not inspect.isclass(value)): if name is not None: - result = "{} {}".format(typename, name) + result = f"{typename} {name}" if article is not None: return add_article(result, True, capital) else: @@ -91,7 +91,7 @@ class name where an object was defined. name = value.__func__.__name__ tick_wrap = True elif type(value).__repr__ in (object.__repr__, type.__repr__): - name = "at '%s'" % hex(id(value)) + name = f"at '{id(value):x}'" verbose = False else: name = repr(value) @@ -107,8 +107,9 @@ class name where an object was defined. return typename return add_article(typename, False, capital) else: - raise ValueError("The 'article' argument should " - "be 'the', 'a', 'an', or None not %r" % article) + raise ValueError( + f"The 'article' argument should be 'the', 'a', 'an', or None not {article!r}" + ) def add_article(name, definite=False, capital=False): @@ -196,8 +197,10 @@ def validate(self, obj, value): try: value = self._resolve_string(value) except ImportError: - raise TraitError("The '%s' trait of %s instance must be a type, but " - "%r could not be imported" % (self.name, obj, value)) + raise TraitError( + f"The '{self.name}' trait of {obj} instance must be a type, " + f"but {value!r} could not be imported" + ) try: if self.subclass_from_klasses(value): return value @@ -283,8 +286,9 @@ class or its subclasses. Our implementation is quite different elif all(inspect.isclass(k) or isinstance(k, str) for k in klasses): self.klasses = klasses else: - raise TraitError('The klasses attribute must be a list of class names or classes' - ' not: %r' % klass) + raise TraitError( + f'The klasses attribute must be a list of class names or classes not: {klass!r}' + ) if (kw is not None) and not isinstance(kw, dict): raise TraitError("The 'kw' argument must be a dict or None.") @@ -294,7 +298,7 @@ class or its subclasses. Our implementation is quite different self.default_args = args self.default_kwargs = kw - super(InstanceFromClasses, self).__init__(**kwargs) + super().__init__(**kwargs) def instance_from_importable_klasses(self, value): "Check that a given class is a subclasses found in the klasses list." diff --git a/notebook/tree/handlers.py b/notebook/tree/handlers.py index bb4958d289..62e0840c0c 100644 --- a/notebook/tree/handlers.py +++ b/notebook/tree/handlers.py @@ -73,6 +73,6 @@ def get(self, path=''): default_handlers = [ - (r"/tree%s" % path_regex, TreeHandler), + (fr"/tree{path_regex}", TreeHandler), (r"/tree", TreeHandler), ] diff --git a/notebook/tree/tests/handlers.py b/notebook/tree/tests/handlers.py index ef42527616..7c37e191fa 100644 --- a/notebook/tree/tests/handlers.py +++ b/notebook/tree/tests/handlers.py @@ -72,6 +72,6 @@ def get(self, path=''): default_handlers = [ - (r"/tree%s" % path_regex, TreeHandler), + (fr"/tree{path_regex}", TreeHandler), (r"/tree", TreeHandler), ] diff --git a/notebook/tree/tests/test_tree_handler.py b/notebook/tree/tests/test_tree_handler.py index 801185ae51..b602b9402b 100644 --- a/notebook/tree/tests/test_tree_handler.py +++ b/notebook/tree/tests/test_tree_handler.py @@ -1,13 +1,10 @@ """Test the /tree handlers""" import os -import io from notebook.utils import url_path_join from nbformat import write from nbformat.v4 import new_notebook from urllib.parse import urlparse -import requests - from notebook.tests.launchnotebook import NotebookTestBase class TreeTest(NotebookTestBase): @@ -16,12 +13,12 @@ def setUp(self): d = os.path.join(nbdir, 'foo') os.mkdir(d) - with io.open(os.path.join(d, 'bar.ipynb'), 'w', encoding='utf-8') as f: + with open(os.path.join(d, 'bar.ipynb'), 'w', encoding='utf-8') as f: nb = new_notebook() write(nb, f, version=4) - with io.open(os.path.join(d, 'baz.txt'), 'w', encoding='utf-8') as f: - f.write(u'flamingo') + with open(os.path.join(d, 'baz.txt'), 'w', encoding='utf-8') as f: + f.write('flamingo') self.base_url() diff --git a/notebook/utils.py b/notebook/utils.py index ef6d9e437c..f7b6380dfe 100644 --- a/notebook/utils.py +++ b/notebook/utils.py @@ -79,14 +79,14 @@ def url_escape(path): Turns '/foo bar/' into '/foo%20bar/' """ parts = py3compat.unicode_to_str(path, encoding='utf8').split('/') - return u'/'.join([quote(p) for p in parts]) + return '/'.join([quote(p) for p in parts]) def url_unescape(path): """Unescape special characters in a URL path Turns '/foo%20bar/' into '/foo bar/' """ - return u'/'.join([ + return '/'.join([ py3compat.str_to_unicode(unquote(p), encoding='utf8') for p in py3compat.unicode_to_str(path, encoding='utf8').split('/') ]) @@ -382,7 +382,7 @@ def urldecode_unix_socket_path(socket_path): def urlencode_unix_socket(socket_path): """Encodes a UNIX socket URL from a socket path for the `http+unix` URI form.""" - return 'http+unix://%s' % urlencode_unix_socket_path(socket_path) + return f'http+unix://{urlencode_unix_socket_path(socket_path)}' def unix_socket_in_use(socket_path): @@ -393,7 +393,7 @@ def unix_socket_in_use(socket_path): try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(socket_path) - except socket.error: + except OSError: return False else: return True diff --git a/notebook/view/handlers.py b/notebook/view/handlers.py index e788964662..2ceed50eb6 100644 --- a/notebook/view/handlers.py +++ b/notebook/view/handlers.py @@ -13,7 +13,7 @@ class ViewHandler(IPythonHandler): def get(self, path): path = path.strip('/') if not self.contents_manager.file_exists(path): - raise web.HTTPError(404, u'File does not exist: %s' % path) + raise web.HTTPError(404, f'File does not exist: {path}') basename = path.rsplit('/', 1)[-1] file_url = url_path_join(self.base_url, 'files', url_escape(path)) @@ -22,5 +22,5 @@ def get(self, path): ) default_handlers = [ - (r"/view%s" % path_regex, ViewHandler), + (fr"/view{path_regex}", ViewHandler), ] diff --git a/setup.py b/setup.py index e255a3129a..628644bbb7 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ pip_message = 'This may be due to an out of date pip. Make sure you have pip >= 9.0.1.' try: import pip - pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]]) + pip_version = tuple(int(x) for x in pip.__version__.split('.')[:3]) if pip_version < (9, 0, 1) : pip_message = 'Your pip version is out of date, please install pip >= 9.0.1. '\ 'pip {} detected.'.format(pip.__version__) @@ -103,10 +103,10 @@ 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9' + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], zip_safe = False, install_requires = [ @@ -136,7 +136,7 @@ 'test:sys_platform != "win32"': ['requests-unixsocket'], 'json-logging': ['json-logging'] }, - python_requires = '>=3.6', + python_requires = '>=3.7', entry_points = { 'console_scripts': [ 'jupyter-notebook = notebook.notebookapp:main', diff --git a/setupbase.py b/setupbase.py index 8849a85e3b..fd69363b9f 100644 --- a/setupbase.py +++ b/setupbase.py @@ -99,7 +99,7 @@ def find_package_data(): """ # This is not enough for these things to appear in a sdist. # We need to muck with the MANIFEST to get this to work - + # exclude components and less from the walk; # we will build the components separately excludes = [ @@ -119,12 +119,12 @@ def find_package_data(): continue for f in files: static_data.append(pjoin(parent, f)) - + # for verification purposes, explicitly add main.min.js # so that installation will fail if they are missing for app in ['auth', 'edit', 'notebook', 'terminal', 'tree']: static_data.append(pjoin('static', app, 'js', 'main.min.js')) - + components = pjoin("static", "components") # select the components we actually need to install # (there are lots of resources we bundle for sdist-reasons that we don't actually use) @@ -173,10 +173,10 @@ def find_package_data(): mj('config', 'TeX-AMS-MML_HTMLorMML-full.js'), mj('config', 'Safe.js'), ]) - + trees = [] mj_out = mj('jax', 'output') - + if os.path.exists(mj_out): for output in os.listdir(mj_out): path = pjoin(mj_out, output) @@ -210,7 +210,7 @@ def find_package_data(): 'notebook.services.api': ['api.yaml'], 'notebook.i18n': ['*/LC_MESSAGES/*.*'], } - + return package_data @@ -229,7 +229,7 @@ def check_package_data(package_data): def check_package_data_first(command): """decorator for checking package_data before running a given command - + Probably only needs to wrap build_py """ class DecoratedCommand(command): @@ -361,21 +361,21 @@ def run(self): class Bower(Command): description = "fetch static client-side components with bower" - + user_options = [ ('force', 'f', "force fetching of bower dependencies"), ] - + def initialize_options(self): self.force = False - + def finalize_options(self): self.force = bool(self.force) - + bower_dir = pjoin(static, 'components') node_modules = pjoin(repo_root, 'node_modules') sanitizer_dir = pjoin(bower_dir, 'sanitizer') - + def should_run(self): if self.force: return True @@ -402,15 +402,15 @@ def run(self): if not self.should_run(): print("bower dependencies up to date") return - + if self.should_run_npm(): print("installing build dependencies with npm") run(['npm', 'install'], cwd=repo_root) os.utime(self.node_modules, None) - + env = os.environ.copy() env['PATH'] = npm_path - + try: run( ['bower', 'install', '--allow-root', '--config.interactive=false'], @@ -453,9 +453,9 @@ def patch_out_bootstrap_bw_print(): class CompileCSS(Command): """Recompile Notebook CSS - + Regenerate the compiled CSS from LESS sources. - + Requires various dev dependencies, such as require and lessc. """ description = "Recompile Notebook CSS" @@ -479,7 +479,7 @@ def run(self): env['PATH'] = npm_path patch_out_bootstrap_bw_print() - + for src, dst in zip(self.sources, self.targets): try: run(['lessc', @@ -498,7 +498,7 @@ def run(self): class CompileJS(Command): """Rebuild Notebook Javascript main.min.js files and translation files. - + Calls require via build-main.js """ description = "Rebuild Notebook Javascript main.min.js files" @@ -514,7 +514,7 @@ def finalize_options(self): apps = ['notebook', 'tree', 'edit', 'terminal', 'auth'] targets = [ pjoin(static, app, 'js', 'main.min.js') for app in apps ] - + def sources(self, name): """Generator yielding .js sources that an application depends on""" yield pjoin(repo_root, 'tools', 'build-main.js') @@ -535,7 +535,7 @@ def sources(self, name): continue for f in files: yield pjoin(parent, f) - + def should_run(self, name, target): if self.force or not os.path.exists(target): return True @@ -582,13 +582,13 @@ class JavascriptVersion(Command): """write the javascript version to notebook javascript""" description = "Write Jupyter version to javascript" user_options = [] - + def initialize_options(self): pass - + def finalize_options(self): pass - + def run(self): nsfile = pjoin(repo_root, "notebook", "static", "base", "js", "namespace.js") with open(nsfile) as f: @@ -597,7 +597,7 @@ def run(self): found = False for line in lines: if line.strip().startswith("Jupyter.version"): - line = ' Jupyter.version = "{0}";\n'.format(version) + line = f' Jupyter.version = "{version}";\n' found = True f.write(line) if not found: diff --git a/tools/secure_notebook.py b/tools/secure_notebook.py index 1984d33946..ec74399556 100644 --- a/tools/secure_notebook.py +++ b/tools/secure_notebook.py @@ -3,12 +3,10 @@ script to automatically setup notebook over SSL. Generate cert and keyfiles (rsa 1024) in ~/.ssh/, ask for a password, and add -the corresponding entries in the notebook json configuration file. +the corresponding entries in the notebook json configuration file. """ -import six - from notebook.auth import passwd from traitlets.config.loader import JSONFileConfigLoader, ConfigFileNotFound from jupyter_core.paths import jupyter_config_dir @@ -19,7 +17,6 @@ from OpenSSL import crypto from os.path import exists, join -import io import os import json import traceback @@ -33,7 +30,7 @@ def create_self_signed_cert(cert_dir, keyfile, certfile): """ if exists(join(cert_dir, certfile)) or exists(join(cert_dir, keyfile)): - raise FileExistsError('{} or {} already exist in {}. Aborting.'.format(keyfile, certfile, cert_dir)) + raise FileExistsError(f'{keyfile} or {certfile} already exist in {cert_dir}. Aborting.') else: # create a key pair k = crypto.PKey() @@ -54,11 +51,11 @@ def create_self_signed_cert(cert_dir, keyfile, certfile): cert.set_pubkey(k) cert.sign(k, 'sha256') - with io.open(join(cert_dir, certfile), "wt") as f: + with open(join(cert_dir, certfile), "wt") as f: f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf8')) os.chmod(join(cert_dir, certfile), 0o600) - - with io.open(join(cert_dir, keyfile), "wt") as f: + + with open(join(cert_dir, keyfile), "wt") as f: f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k).decode('utf8')) os.chmod(join(cert_dir, keyfile), 0o600) @@ -68,7 +65,7 @@ def create_self_signed_cert(cert_dir, keyfile, certfile): def persist_config(mode=0o600): """Context manager that can be use to modify a config object - On exit of the context manager, the config will be written back to disk, + On exit of the context manager, the config will be written back to disk, by default with 600 permissions. """ @@ -81,8 +78,8 @@ def persist_config(mode=0o600): yield config filepath = os.path.join(jupyter_config_dir(), 'jupyter_notebook_config.json') - with io.open(filepath, 'w') as f: - f.write(six.u(json.dumps(config, indent=2))) + with open(filepath, 'w') as f: + f.write(json.dumps(config, indent=2)) try: os.chmod(filepath, mode) except Exception: From e3aea457b954f9a91b73070886a43d34506152c5 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 12 Apr 2022 08:10:23 +0200 Subject: [PATCH 14/78] temporary workaround to fix js-tests related to sanitizer js loading by phantomjs --- .github/workflows/js.yml | 4 + bower.json | 1 + setupbase.py | 1 + tools/security_deprecated.js | 152 +++++++++++++++++++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 tools/security_deprecated.js diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 9ce5424e20..96c5795cdb 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -56,6 +56,10 @@ jobs: npm install -g casperjs@1.1.3 phantomjs-prebuilt@2.1.7 pip install .[test] + - name: Temporary workaround for sanitizer loading in JS Tests + run: | + cp tools/security_deprecated.js notebook/static/base/js/security.js + - name: Run Tests run: | python -m notebook.jstest ${{ matrix.group }} diff --git a/bower.json b/bower.json index 393db02ad1..5f72f6ff04 100644 --- a/bower.json +++ b/bower.json @@ -9,6 +9,7 @@ "create-react-class": "https://cdn.jsdelivr.net/npm/create-react-class@15.6.3/create-react-class.min.js", "es6-promise": "~1.0", "font-awesome": "components/font-awesome#~4.7.0", + "google-caja": "5669", "jed": "~1.1.1", "jquery": "components/jquery#~3.5.0", "jquery-typeahead": "~2.10.6", diff --git a/setupbase.py b/setupbase.py index fd69363b9f..ebccd2ac79 100644 --- a/setupbase.py +++ b/setupbase.py @@ -135,6 +135,7 @@ def find_package_data(): pjoin(components, "bootstrap-tour", "build", "js", "bootstrap-tour.min.js"), pjoin(components, "create-react-class", "index.js"), pjoin(components, "font-awesome", "css", "*.css"), + pjoin(components, "google-caja", "html-css-sanitizer-minified.js"), pjoin(components, "es6-promise", "*.js"), pjoin(components, "font-awesome", "fonts", "*.*"), pjoin(components, "jed", "jed.js"), diff --git a/tools/security_deprecated.js b/tools/security_deprecated.js new file mode 100644 index 0000000000..195c0a5cb0 --- /dev/null +++ b/tools/security_deprecated.js @@ -0,0 +1,152 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'jquery', + 'components/google-caja/html-css-sanitizer-minified', +], function($, sanitize) { + "use strict"; + + var noop = function (x) { return x; }; + + var caja; + if (window && window.html) { + caja = window.html; + caja.html4 = window.html4; + caja.sanitizeStylesheet = window.sanitizeStylesheet; + } + + var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) { + /** + * add trusting data-attributes to the default sanitizeAttribs from caja + * this function is mostly copied from the caja source + */ + var ATTRIBS = caja.html4.ATTRIBS; + for (var i = 0; i < attribs.length; i += 2) { + var attribName = attribs[i]; + if (attribName.substr(0,5) == 'data-') { + var attribKey = '*::' + attribName; + if (!ATTRIBS.hasOwnProperty(attribKey)) { + ATTRIBS[attribKey] = 0; + } + } + } + // Caja doesn't allow data uri for img::src, see + // https://github.com/google/caja/issues/1558 + // This is not a security issue for browser post ie6 though, so we + // disable the check + // https://www.owasp.org/index.php/Script_in_IMG_tags + ATTRIBS['img::src'] = 0; + return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger); + }; + + var sanitize_css = function (css, tagPolicy) { + /** + * sanitize CSS + * like sanitize_html, but for CSS + * called by sanitize_stylesheets + */ + return caja.sanitizeStylesheet( + window.location.pathname, + css, + { + containerClass: null, + idSuffix: '', + tagPolicy: tagPolicy, + virtualizeAttrName: noop + }, + noop + ); + }; + + var sanitize_stylesheets = function (html, tagPolicy) { + /** + * sanitize just the css in style tags in a block of html + * called by sanitize_html, if allow_css is true + */ + var h = $("
").append(html); + var style_tags = h.find("style"); + if (!style_tags.length) { + // no style tags to sanitize + return html; + } + style_tags.each(function(i, style) { + style.innerHTML = sanitize_css(style.innerHTML, tagPolicy); + }); + return h.html(); + }; + + var sanitize_html = function (html, allow_css) { + /** + * sanitize HTML + * if allow_css is true (default: false), CSS is sanitized as well. + * otherwise, CSS elements and attributes are simply removed. + */ + var html4 = caja.html4; + + if (allow_css) { + // allow sanitization of style tags, + // not just scrubbing + html4.ELEMENTS.style &= ~html4.eflags.UNSAFE; + html4.ATTRIBS.style = html4.atype.STYLE; + } else { + // scrub all CSS + html4.ELEMENTS.style |= html4.eflags.UNSAFE; + html4.ATTRIBS.style = html4.atype.SCRIPT; + } + + var record_messages = function (msg, opts) { + console.log("HTML Sanitizer", msg, opts); + }; + + var policy = function (tagName, attribs) { + if (!(html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) { + return { + 'attribs': sanitizeAttribs(tagName, attribs, + noop, noop, record_messages) + }; + } else { + record_messages(tagName + " removed", { + change: "removed", + tagName: tagName + }); + } + }; + + var sanitized = caja.sanitizeWithPolicy(html, policy); + + if (allow_css) { + // sanitize style tags as stylesheets + sanitized = sanitize_stylesheets(sanitized, policy); + } + + return sanitized; + }; + + var sanitize_html_and_parse = function (html, allow_css) { + /** + * Sanitize HTML and parse it safely using jQuery. + * + * This disable's jQuery's html 'prefilter', which can make invalid + * HTML valid after the sanitizer has checked it. + * + * Returns an array of DOM nodes. + */ + var sanitized_html = sanitize_html(html, allow_css); + var prev_htmlPrefilter = $.htmlPrefilter; + $.htmlPrefilter = function(html) {return html;}; // Don't modify HTML + try { + return $.parseHTML(sanitized_html); + } finally { + $.htmlPrefilter = prev_htmlPrefilter; // Set it back again + } + }; + + var security = { + caja: caja, + sanitize_html_and_parse: sanitize_html_and_parse, + sanitize_html: sanitize_html + }; + + return security; +}); From 9d67de4ce05a195ca87c8ac432afd8c22b310780 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 12 Apr 2022 08:28:10 +0200 Subject: [PATCH 15/78] install workaround before installing with pip --- .github/workflows/js.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 96c5795cdb..6f1bf4c383 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -48,6 +48,10 @@ jobs: restore-keys: | ${{ runner.os }}-pip-${{ matrix.python }} + - name: Temporary workaround for sanitizer loading in JS Tests + run: | + cp tools/security_deprecated.js notebook/static/base/js/security.js + - name: Install dependencies run: | pip install --upgrade pip @@ -56,10 +60,6 @@ jobs: npm install -g casperjs@1.1.3 phantomjs-prebuilt@2.1.7 pip install .[test] - - name: Temporary workaround for sanitizer loading in JS Tests - run: | - cp tools/security_deprecated.js notebook/static/base/js/security.js - - name: Run Tests run: | python -m notebook.jstest ${{ matrix.group }} From 6b82d593363be418cfe83c2e7becb9f468f1cf41 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 12 Apr 2022 08:45:38 +0200 Subject: [PATCH 16/78] add testpath to test deps --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 628644bbb7..7f46a7e1a5 100755 --- a/setup.py +++ b/setup.py @@ -129,7 +129,7 @@ 'prometheus_client' ], extras_require = { - 'test': ['pytest', 'coverage', 'requests', + 'test': ['pytest', 'coverage', 'requests', 'testpath', 'nbval', 'selenium', 'pytest', 'pytest-cov'], 'docs': ['sphinx', 'nbsphinx', 'sphinxcontrib_github_alt', 'sphinx_rtd_theme', 'myst-parser'], From fbeaa977049e6ac2723b98017ab1055c8cb603fc Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 12 Apr 2022 12:09:15 +0200 Subject: [PATCH 17/78] Update further to ipykernel comm refatoring --- docs/source/comms.rst | 2 +- notebook/tests/services/serialize.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/comms.rst b/docs/source/comms.rst index 7c292d78bd..c2ff11bb52 100644 --- a/docs/source/comms.rst +++ b/docs/source/comms.rst @@ -78,7 +78,7 @@ containing Javascript to connect to it. # Send data to the frontend on creation comm.send({'foo': 5}) - get_ipython().kernel.comm_manager.register_target('my_comm_target', target_func) + get_ipython().comm.comm_manager.register_target('my_comm_target', target_func) This example uses the IPython kernel again; this example will be different in other kernels that support comms. Refer to the specific language kernel's diff --git a/notebook/tests/services/serialize.js b/notebook/tests/services/serialize.js index e44ea47043..cd538c54dc 100644 --- a/notebook/tests/services/serialize.js +++ b/notebook/tests/services/serialize.js @@ -33,7 +33,7 @@ casper.notebook_test(function () { this.then(function () { var index = this.append_cell([ "import os", - "from IPython.kernel.comm import Comm", + "from IPython.comm.comm import Comm", "comm = Comm(target_name='echo')", "msgs = []", "def on_msg(msg):", From 5ca18c2170626602507fc5eec038deb0d0d82041 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 12 Apr 2022 12:19:48 +0200 Subject: [PATCH 18/78] more fixes for ipykernel refactoring --- notebook/tests/services/kernel.js | 2 +- notebook/tests/services/serialize.js | 2 +- notebook/tests/services/session.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/tests/services/kernel.js b/notebook/tests/services/kernel.js index 4f22b802e9..a7e5382632 100644 --- a/notebook/tests/services/kernel.js +++ b/notebook/tests/services/kernel.js @@ -310,7 +310,7 @@ casper.notebook_test(function () { this.thenEvaluate(function () { var cell = IPython.notebook.get_cell(0); cell.set_text("import os\n" + - "from IPython.kernel.connect import get_connection_file\n" + + "from ipykernel.connect import get_connection_file\n" + "with open(get_connection_file(), 'w') as f:\n" + " f.write('garbage')\n" + "os._exit(1)"); diff --git a/notebook/tests/services/serialize.js b/notebook/tests/services/serialize.js index cd538c54dc..36bb165659 100644 --- a/notebook/tests/services/serialize.js +++ b/notebook/tests/services/serialize.js @@ -33,7 +33,7 @@ casper.notebook_test(function () { this.then(function () { var index = this.append_cell([ "import os", - "from IPython.comm.comm import Comm", + "from ipykernel.comm.comm import Comm", "comm = Comm(target_name='echo')", "msgs = []", "def on_msg(msg):", diff --git a/notebook/tests/services/session.js b/notebook/tests/services/session.js index 5f41278de0..ff1fa25d35 100644 --- a/notebook/tests/services/session.js +++ b/notebook/tests/services/session.js @@ -154,7 +154,7 @@ casper.notebook_test(function () { this.thenEvaluate(function () { var cell = IPython.notebook.get_cell(0); cell.set_text("import os\n" + - "from IPython.kernel.connect import get_connection_file\n" + + "from ipykernel.connect import get_connection_file\n" + "with open(get_connection_file(), 'w') as f:\n" + " f.write('garbage')\n" + "os._exit(1)"); From 45e2f2de744332cf5e5d9a86a417f8d401e89363 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 12 Apr 2022 12:25:15 +0200 Subject: [PATCH 19/78] revert bad fix for comms rst --- docs/source/comms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/comms.rst b/docs/source/comms.rst index c2ff11bb52..7c292d78bd 100644 --- a/docs/source/comms.rst +++ b/docs/source/comms.rst @@ -78,7 +78,7 @@ containing Javascript to connect to it. # Send data to the frontend on creation comm.send({'foo': 5}) - get_ipython().comm.comm_manager.register_target('my_comm_target', target_func) + get_ipython().kernel.comm_manager.register_target('my_comm_target', target_func) This example uses the IPython kernel again; this example will be different in other kernels that support comms. Refer to the specific language kernel's From 862d73a7226474440fefdb5b31c6f953f010b48f Mon Sep 17 00:00:00 2001 From: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Apr 2022 15:38:36 +0000 Subject: [PATCH 20/78] Automated Changelog Entry for 6.4.10 on 6.4.x --- CHANGELOG.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e02ff8e801..044a7120f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,29 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with +## 6.4.10 + +([Full Changelog](https://github.com/jupyter/notebook/compare/6.4.10...3911672959fcd35cf4a1b1ad7c9c8a5651c17ae6)) + +### Bugs fixed + +- Update further to ipykernel comm refactoring [#6358](https://github.com/jupyter/notebook/pull/6358) ([@echarles](https://github.com/echarles)) + +### Maintenance and upkeep improvements + +- Add testpath to the test dependencies. [#6357](https://github.com/jupyter/notebook/pull/6357) ([@echarles](https://github.com/echarles)) +- Temporary workaround to fix js-tests related to sanitizer js loading by phantomjs [#6356](https://github.com/jupyter/notebook/pull/6356) ([@echarles](https://github.com/echarles)) +- Use place-hold.it instead of plaecehold.it to create image placeholders [#6320](https://github.com/jupyter/notebook/pull/6320) ([@echarles](https://github.com/echarles)) +- Fix #6256 : migrate to python 3.7+ [#6260](https://github.com/jupyter/notebook/pull/6260) ([@penguinolog](https://github.com/penguinolog)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/jupyter/notebook/graphs/contributors?from=2022-03-15&to=2022-04-18&type=c)) + +[@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ablink1073+updated%3A2022-03-15..2022-04-18&type=Issues) | [@echarles](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Aecharles+updated%3A2022-03-15..2022-04-18&type=Issues) | [@fcollonval](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Afcollonval+updated%3A2022-03-15..2022-04-18&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2022-03-15..2022-04-18&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ajtpio+updated%3A2022-03-15..2022-04-18&type=Issues) | [@penguinolog](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Apenguinolog+updated%3A2022-03-15..2022-04-18&type=Issues) + + + ## 6.4.9 ([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...9e3a7001117e64a24ead07b888bd055fdd66faf3)) @@ -33,8 +56,6 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with [@adamjstewart](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Aadamjstewart+updated%3A2022-01-12..2022-03-14&type=Issues) | [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ablink1073+updated%3A2022-01-12..2022-03-14&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2022-01-12..2022-03-14&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ajtpio+updated%3A2022-01-12..2022-03-14&type=Issues) | [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Akevin-bates+updated%3A2022-01-12..2022-03-14&type=Issues) | [@kycutler](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Akycutler+updated%3A2022-01-12..2022-03-14&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3AZsailer+updated%3A2022-01-12..2022-03-14&type=Issues) - - ## 6.4.8 ([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...479902d83a691253e0cff8439a33577e82408317)) From 5fbcf21defab986d95036ebd2d5536c0d111e9f7 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 18 Apr 2022 17:42:48 +0200 Subject: [PATCH 21/78] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 044a7120f1..b80bb438bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with - Add testpath to the test dependencies. [#6357](https://github.com/jupyter/notebook/pull/6357) ([@echarles](https://github.com/echarles)) - Temporary workaround to fix js-tests related to sanitizer js loading by phantomjs [#6356](https://github.com/jupyter/notebook/pull/6356) ([@echarles](https://github.com/echarles)) - Use place-hold.it instead of plaecehold.it to create image placeholders [#6320](https://github.com/jupyter/notebook/pull/6320) ([@echarles](https://github.com/echarles)) -- Fix #6256 : migrate to python 3.7+ [#6260](https://github.com/jupyter/notebook/pull/6260) ([@penguinolog](https://github.com/penguinolog)) +- Migrate to python 3.7+ [#6260](https://github.com/jupyter/notebook/pull/6260) - Fixes [#6256](https://github.com/jupyter/notebook/pull/6256) ([@penguinolog](https://github.com/penguinolog)) ### Contributors to this release From 58f5bbb32b20c93a473a816b2b67b5c96160b1f8 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 18 Apr 2022 18:13:09 +0200 Subject: [PATCH 22/78] Fix changelog for 6.4.11 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b80bb438bf..6c477f733c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with -## 6.4.10 +## 6.4.11 ([Full Changelog](https://github.com/jupyter/notebook/compare/6.4.10...3911672959fcd35cf4a1b1ad7c9c8a5651c17ae6)) From bb5db37d4181d665dd2d906a9412fb53a5da81e9 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 19 Apr 2022 04:41:41 +0200 Subject: [PATCH 23/78] back to 6.4.10 main version, instead of 6.5.0.dev0 --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index 6c5729f54d..d9dadc3254 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.5.0.dev0' +__version__ = '6.4.10' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 5eeb1ebd0d..64e28db7e6 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.5.0.dev0"; + Jupyter.version = "6.4.10"; Jupyter._target = '_blank'; return Jupyter; diff --git a/pyproject.toml b/pyproject.toml index 9553029ced..c6f66a60e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.5.0.dev0" +current = "6.4.10" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From 8109251477fd813c1b94c6b0c7fbb9e051512b44 Mon Sep 17 00:00:00 2001 From: GitHub Action <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 Apr 2022 02:55:33 +0000 Subject: [PATCH 24/78] Publish 6.4.11 SHA256 hashes: notebook-6.4.11-py3-none-any.whl: b4a6baf2eba21ce67a0ca11a793d1781b06b8078f34d06c710742e55f3eee505 notebook-6.4.11.tar.gz: 709b1856a564fe53054796c80e17a67262071c86bfbdfa6b96aaa346113c555a --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index d9dadc3254..4ddfb4b623 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.4.10' +__version__ = '6.4.11' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 64e28db7e6..24b2ac02e4 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.4.10"; + Jupyter.version = "6.4.11"; Jupyter._target = '_blank'; return Jupyter; diff --git a/pyproject.toml b/pyproject.toml index c6f66a60e6..184a297c79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.4.10" +current = "6.4.11" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From d4eb85d0f9e2845d0889ef8a19486b96c850cba9 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 12 May 2022 10:01:48 -0700 Subject: [PATCH 25/78] updating version to show dev --- notebook/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/_version.py b/notebook/_version.py index 4ddfb4b623..678319de40 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.4.11' +__version__ = '6.4.12.dev0' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' From 2a76184f46ed6a7afcdd01e9ba660010abb9c977 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 18 May 2022 09:38:52 -0400 Subject: [PATCH 26/78] add checks for hidden file or path on file get --- notebook/services/contents/filemanager.py | 7 ++++ .../services/contents/tests/test_manager.py | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index b517ca4702..f635d793fd 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -363,6 +363,13 @@ def _file_model(self, path, content=True, format=None): model['type'] = 'file' os_path = self._get_os_path(path) + + four_o_four = "file does not exist: %r" % path + + if is_hidden(os_path, self.root_dir) and not self.allow_hidden: + self.log.info("Refusing to serve hidden file or file in hidden directory %r, via 404 Error", os_path) + raise web.HTTPError(404, four_o_four) + model['mimetype'] = mimetypes.guess_type(os_path)[0] if content: diff --git a/notebook/services/contents/tests/test_manager.py b/notebook/services/contents/tests/test_manager.py index cac98e893d..4e65ee90f3 100644 --- a/notebook/services/contents/tests/test_manager.py +++ b/notebook/services/contents/tests/test_manager.py @@ -181,6 +181,41 @@ def test_403(self): else: self.fail("Should have raised HTTPError(403)") + @skipIf(sys.platform.startswith('win'), "Can't test hidden files on Windows") + def test_404(self): + #Test visible file in hidden folder + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = '.hidden' + file_in_hidden_path = os.path.join(hidden_dir,'visible.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + os_path = cm._get_os_path(model['path']) + + try: + result = cm.get(os_path, 'w') + except HTTPError as e: + self.assertEqual(e.status_code, 404) + else: + self.fail("Should have raised HTTPError(404)") + #Test hidden file in visible folder + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = 'visible' + file_in_hidden_path = os.path.join(hidden_dir,'.hidden.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + os_path = cm._get_os_path(model['path']) + + try: + result = cm.get(os_path, 'w') + except HTTPError as e: + self.assertEqual(e.status_code, 404) + else: + self.fail("Should have raised HTTPError(404)") + def test_escape_root(self): with TemporaryDirectory() as td: cm = FileContentsManager(root_dir=td) From f69eb96cff7149b38bca068f4b7faaa7baf55902 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 20 May 2022 12:33:45 -0400 Subject: [PATCH 27/78] added hidden checks on FileContentsManager and accompanying tests --- notebook/services/contents/filemanager.py | 39 +++-- .../services/contents/tests/test_manager.py | 143 ++++++++++++++++++ 2 files changed, 172 insertions(+), 10 deletions(-) diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index f635d793fd..f9aa2cefb0 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -242,6 +242,12 @@ def _base_model(self, path): os_path = self._get_os_path(path) info = os.lstat(os_path) + four_o_four = "file or directory does not exist: %r" % path + + if is_hidden(os_path, self.root_dir) and not self.allow_hidden: + self.log.info("Refusing to serve hidden file or file in hidden directory %r, via 404 Error", os_path) + raise web.HTTPError(404, four_o_four) + try: # size of file size = info.st_size @@ -364,12 +370,6 @@ def _file_model(self, path, content=True, format=None): os_path = self._get_os_path(path) - four_o_four = "file does not exist: %r" % path - - if is_hidden(os_path, self.root_dir) and not self.allow_hidden: - self.log.info("Refusing to serve hidden file or file in hidden directory %r, via 404 Error", os_path) - raise web.HTTPError(404, four_o_four) - model['mimetype'] = mimetypes.guess_type(os_path)[0] if content: @@ -430,11 +430,17 @@ def get(self, path, content=True, type=None, format=None): of the file or directory as well. """ path = path.strip('/') + os_path = self._get_os_path(path) + four_o_four = "file or directory does not exist: %r" % path if not self.exists(path): - raise web.HTTPError(404, f'No such file or directory: {path}') + raise web.HTTPError(404, four_o_four) - os_path = self._get_os_path(path) + if is_hidden(os_path, self.root_dir) and not self.allow_hidden: + self.log.info("Refusing to serve hidden file or file in hidden directory %r, via 404 Error", os_path) + raise web.HTTPError(404, four_o_four) + + if os.path.isdir(os_path): if type not in (None, 'directory'): raise web.HTTPError(400, @@ -472,6 +478,10 @@ def save(self, model, path=''): raise web.HTTPError(400, 'No file content provided') os_path = self._get_os_path(path) + + if is_hidden(os_path, self.root_dir) and not self.allow_hidden: + raise web.HTTPError(400, f'Cannot create hidden file or directory {os_path!r}') + self.log.debug("Saving %s", os_path) self.run_pre_save_hook(model=model, path=path) @@ -515,8 +525,14 @@ def delete_file(self, path): path = path.strip('/') os_path = self._get_os_path(path) rm = os.unlink - if not os.path.exists(os_path): - raise web.HTTPError(404, f'File or directory does not exist: {os_path}') + + four_o_four = "file or directory does not exist: %r" % path + + if not self.exists(path): + raise web.HTTPError(404, four_o_four) + + if is_hidden(os_path, self.root_dir) and not self.allow_hidden: + raise web.HTTPError(400, f'Cannot delete hidden file or directory {os_path!r}') def is_non_empty_dir(os_path): if os.path.isdir(os_path): @@ -559,6 +575,9 @@ def rename_file(self, old_path, new_path): if new_path == old_path: return + if (is_hidden(old_path, self.root_dir) or is_hidden(new_path, self.root_dir)) and not self.allow_hidden: + raise web.HTTPError(400, f'Cannot rename hidden file or directory {os_path!r}') + # Perform path validation prior to converting to os-specific value since this # is still relative to root_dir. self._validate_path(new_path) diff --git a/notebook/services/contents/tests/test_manager.py b/notebook/services/contents/tests/test_manager.py index 4e65ee90f3..05043d6888 100644 --- a/notebook/services/contents/tests/test_manager.py +++ b/notebook/services/contents/tests/test_manager.py @@ -181,6 +181,149 @@ def test_403(self): else: self.fail("Should have raised HTTPError(403)") + @skipIf(sys.platform.startswith('win'), "Can't test hidden files on Windows") + def test_400(self): + #Test Delete behavior + #Test delete of file in hidden directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = '.hidden' + file_in_hidden_path = os.path.join(hidden_dir,'visible.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + os_path = cm._get_os_path(model['path']) + + try: + result = cm.delete_file(os_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + #Test delete hidden file in visible directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = 'visible' + file_in_hidden_path = os.path.join(hidden_dir,'.hidden.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + os_path = cm._get_os_path(model['path']) + + try: + result = cm.delete_file(os_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + + #Test Save behavior + #Test save of file in hidden directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = '.hidden' + file_in_hidden_path = os.path.join(hidden_dir,'visible.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + os_path = cm._get_os_path(model['path']) + + try: + result = cm.save(model,path=os_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + #Test save hidden file in visible directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = 'visible' + file_in_hidden_path = os.path.join(hidden_dir,'.hidden.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + os_path = cm._get_os_path(model['path']) + + try: + result = cm.save(model,path=os_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + + #Test rename behavior + #Test rename with source file in hidden directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = '.hidden' + file_in_hidden_path = os.path.join(hidden_dir,'visible.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + old_path = cm._get_os_path(model['path']) + new_path = "new.txt" + + try: + result = cm.rename_file(old_path, new_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + + #Test rename of dest file in hidden directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = '.hidden' + file_in_hidden_path = os.path.join(hidden_dir,'visible.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + new_path = cm._get_os_path(model['path']) + old_path = "old.txt" + + try: + result = cm.rename_file(old_path, new_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + + #Test rename with hidden source file in visible directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = 'visible' + file_in_hidden_path = os.path.join(hidden_dir,'.hidden.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + old_path = cm._get_os_path(model['path']) + new_path = "new.txt" + + try: + result = cm.rename_file(old_path, new_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + + #Test rename with hidden dest file in visible directory + with self.assertRaises(HTTPError) as excinfo: + with TemporaryDirectory() as td: + cm = FileContentsManager(root_dir=td) + hidden_dir = 'visible' + file_in_hidden_path = os.path.join(hidden_dir,'.hidden.txt') + _make_dir(cm, hidden_dir) + model = cm.new(path=file_in_hidden_path) + new_path = cm._get_os_path(model['path']) + old_path = "old.txt" + + try: + result = cm.rename_file(old_path, new_path) + except HTTPError as e: + self.assertEqual(e.status_code, 400) + else: + self.fail("Should have raised HTTPError(400)") + @skipIf(sys.platform.startswith('win'), "Can't test hidden files on Windows") def test_404(self): #Test visible file in hidden folder From 1c3d7a60cd16deddba5b1344f5c94f967ba682d8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 20 May 2022 15:27:54 -0400 Subject: [PATCH 28/78] added hidden checks on handlers.py and accompanying tests --- notebook/services/contents/handlers.py | 17 ++- .../contents/tests/test_contents_api.py | 120 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/notebook/services/contents/handlers.py b/notebook/services/contents/handlers.py index 1a8f2a2aab..4c9158d182 100644 --- a/notebook/services/contents/handlers.py +++ b/notebook/services/contents/handlers.py @@ -101,6 +101,7 @@ def get(self, path=''): of the files and directories it contains. """ path = path or '' + cm = self.contents_manager type = self.get_query_argument('type', default=None) if type not in {None, 'directory', 'file', 'notebook'}: raise web.HTTPError(400, f'Type {type!r} is invalid') @@ -112,7 +113,8 @@ def get(self, path=''): if content not in {'0', '1'}: raise web.HTTPError(400, f'Content {content!r} is invalid') content = int(content) - + if cm.is_hidden(path) and not cm.allow_hidden: + raise web.HTTPError(404, f'file or directory {path!r} does not exist') model = yield maybe_future(self.contents_manager.get( path=path, type=type, format=format, content=content, )) @@ -125,6 +127,9 @@ def patch(self, path=''): """PATCH renames a file or directory without re-uploading content.""" cm = self.contents_manager model = self.get_json_body() + old_path = model.get('path') + if old_path and (cm.is_hidden(path) or cm.is_hidden(old_path)) and not cm.allow_hidden: + raise web.HTTPError(400, f'Cannot rename hidden file or directory {path!r}') if model is None: raise web.HTTPError(400, 'JSON body missing') model = yield maybe_future(cm.update(model, path)) @@ -193,6 +198,9 @@ def post(self, path=''): raise web.HTTPError(404, f"No such directory: {path}") model = self.get_json_body() + copy_from = model.get('copy_from') + if copy_from and (cm.is_hidden(path) or cm.is_hidden(copy_from)) and not cm.allow_hidden: + raise web.HTTPError(400, f'Cannot copy hidden file or directory {path!r}') if model is not None: copy_from = model.get('copy_from') @@ -219,9 +227,12 @@ def put(self, path=''): create a new empty notebook. """ model = self.get_json_body() + cm = self.contents_manager if model: if model.get('copy_from'): raise web.HTTPError(400, "Cannot copy with PUT, only POST") + if model.get('path') and (cm.is_hidden(path) or cm.is_hidden(model.get('path'))) and not cm.allow_hidden: + raise web.HTTPError(400, f'Cannot create hidden file or directory {path!r}') exists = yield maybe_future(self.contents_manager.file_exists(path)) if exists: yield maybe_future(self._save(model, path)) @@ -235,6 +246,10 @@ def put(self, path=''): def delete(self, path=''): """delete a file in the given path""" cm = self.contents_manager + + if cm.is_hidden(path) and not cm.allow_hidden: + raise web.HTTPError(400, f'Cannot delete hidden file or directory {path!r}') + self.log.warning('delete %s', path) yield maybe_future(cm.delete(path)) self.set_status(204) diff --git a/notebook/services/contents/tests/test_contents_api.py b/notebook/services/contents/tests/test_contents_api.py index 592d22365b..e0a37aae94 100644 --- a/notebook/services/contents/tests/test_contents_api.py +++ b/notebook/services/contents/tests/test_contents_api.py @@ -321,6 +321,16 @@ def test_get_contents_no_such_file(self): with assert_http_error(404): self.api.read('foo/q.ipynb') + def test_get_404_hidden(self): + if sys.platform == 'win32': + self.skipTest("Disabled copying hidden files on Windows") + self.make_txt('.hidden/visible.txt', 'test string') + self.make_txt('.hidden.txt', 'test string') + with assert_http_error(404): + resp = self.api.read('.hidden/visible.txt') + with assert_http_error(404): + resp = self.api.read('.hidden.txt') + def test_get_text_file_contents(self): for d, name in self.dirs_nbs: path = url_path_join(d, name + '.txt') @@ -443,6 +453,51 @@ def test_upload_txt(self): self.assertEqual(model['format'], 'text') self.assertEqual(model['content'], body) + def test_upload_txt_hidden(self): + if sys.platform == 'win32': + self.skipTest("Disabled copying hidden files on Windows") + with assert_http_error(400): + body = 'ünicode téxt' + model = { + 'content' : body, + 'format' : 'text', + 'type' : 'file', + } + path = '.hidden/Upload tést.txt' + resp = self.api.upload(path, body=json.dumps(model)) + + with assert_http_error(400): + body = 'ünicode téxt' + model = { + 'content' : body, + 'format' : 'text', + 'type' : 'file', + 'path': '.hidden/test.txt' + } + path = 'Upload tést.txt' + resp = self.api.upload(path, body=json.dumps(model)) + + with assert_http_error(400): + body = 'ünicode téxt' + model = { + 'content' : body, + 'format' : 'text', + 'type' : 'file', + } + path = '.hidden.txt' + resp = self.api.upload(path, body=json.dumps(model)) + + with assert_http_error(400): + body = 'ünicode téxt' + model = { + 'content' : body, + 'format' : 'text', + 'type' : 'file', + 'path': '.hidden.txt' + } + path = 'Upload tést.txt' + resp = self.api.upload(path, body=json.dumps(model)) + def test_upload_b64(self): body = b'\xFFblob' b64body = encodebytes(body).decode('ascii') @@ -497,10 +552,37 @@ def test_copy_path(self): resp = self.api.copy('foo/a.ipynb', 'å b') self._check_created(resp, 'å b/a-Copy1.ipynb') + def test_copy_400_hidden(self): + if sys.platform == 'win32': + self.skipTest("Disabled copying hidden files on Windows") + self.make_txt('new.txt', 'test string') + self.make_txt('.hidden/new.txt', 'test string') + self.make_txt('.hidden.txt', 'test string') + with assert_http_error(400): + resp = self.api.copy('.hidden/old.txt', 'new.txt') + with assert_http_error(400): + resp = self.api.copy('old.txt', '.hidden/new.txt') + with assert_http_error(400): + resp = self.api.copy('.hidden.txt', 'new.txt') + with assert_http_error(400): + resp = self.api.copy('old.txt', '.hidden.txt') + def test_copy_put_400(self): with assert_http_error(400): resp = self.api.copy_put('å b/ç d.ipynb', 'å b/cøpy.ipynb') + def test_copy_put_400_hidden(self): + if sys.platform == 'win32': + self.skipTest("Disabled copying hidden files on Windows") + with assert_http_error(400): + resp = self.api.copy_put('.hidden/old.txt', 'new.txt') + with assert_http_error(400): + resp = self.api.copy_put('old.txt', '.hidden/new.txt') + with assert_http_error(400): + resp = self.api.copy_put('.hidden.txt', 'new.txt') + with assert_http_error(400): + resp = self.api.copy_put('old.txt', '.hidden.txt') + def test_copy_dir_400(self): # can't copy directories with assert_http_error(400): @@ -543,6 +625,29 @@ def test_delete_non_empty_dir(self): with assert_http_error(404): self.api.list('å b') + def test_delete_hidden_dir(self): + if sys.platform == 'win32': + self.skipTest("Disabled deleting hidden dirs on Windows") + with assert_http_error(400): + # Test that non empty directory can be deleted + try: + self.api.delete('.hidden') + except requests.HTTPError as e: + assert e.response.status_code == 400 + raise e + + def test_delete_hidden_file(self): + #Test deleting file in a hidden directory + if sys.platform == 'win32': + self.skipTest("Disabled deleting hidden dirs on Windows") + with assert_http_error(400): + # Test that non empty directory can be deleted + self.api.delete('.hidden/test.txt') + + #Test deleting a hidden file + with assert_http_error(400): + self.api.delete('.hidden.txt') + def test_rename(self): resp = self.api.rename('foo/a.ipynb', 'foo/z.ipynb') self.assertEqual(resp.headers['Location'].split('/')[-1], 'z.ipynb') @@ -555,6 +660,21 @@ def test_rename(self): self.assertIn('z.ipynb', nbnames) self.assertNotIn('a.ipynb', nbnames) + def test_rename_400_hidden(self): + if sys.platform == 'win32': + self.skipTest("Disabled copying hidden files on Windows") + # self.make_txt('new.txt', 'test string') + # self.make_txt('.hidden/new.txt', 'test string') + # self.make_txt('.hidden.txt', 'test string') + with assert_http_error(400): + resp = self.api.rename('.hidden/old.txt', 'new.txt') + with assert_http_error(400): + resp = self.api.rename('old.txt', '.hidden/new.txt') + with assert_http_error(400): + resp = self.api.rename('.hidden.txt', 'new.txt') + with assert_http_error(400): + resp = self.api.rename('old.txt', '.hidden.txt') + def test_checkpoints_follow_file(self): # Read initial file state From cb3dc22f0f152c1dcbcdb1aa3514748758f432a8 Mon Sep 17 00:00:00 2001 From: Ryan <37374243+rashley-iqt@users.noreply.github.com> Date: Wed, 25 May 2022 12:36:32 -0400 Subject: [PATCH 29/78] Update notebook/services/contents/filemanager.py Co-authored-by: Min RK --- notebook/services/contents/filemanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index f9aa2cefb0..e69ee7a48a 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -245,7 +245,7 @@ def _base_model(self, path): four_o_four = "file or directory does not exist: %r" % path if is_hidden(os_path, self.root_dir) and not self.allow_hidden: - self.log.info("Refusing to serve hidden file or file in hidden directory %r, via 404 Error", os_path) + self.log.info("Refusing to serve hidden file or directory %r, via 404 Error", os_path) raise web.HTTPError(404, four_o_four) try: From b79702ccdbc40a1fda5a87a3f73854e8267529e6 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 25 May 2022 13:05:48 -0400 Subject: [PATCH 30/78] updated error messages to not mention hidden files --- notebook/services/contents/filemanager.py | 10 +++++----- notebook/services/contents/handlers.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/notebook/services/contents/filemanager.py b/notebook/services/contents/filemanager.py index e69ee7a48a..5754e94074 100644 --- a/notebook/services/contents/filemanager.py +++ b/notebook/services/contents/filemanager.py @@ -437,7 +437,7 @@ def get(self, path, content=True, type=None, format=None): raise web.HTTPError(404, four_o_four) if is_hidden(os_path, self.root_dir) and not self.allow_hidden: - self.log.info("Refusing to serve hidden file or file in hidden directory %r, via 404 Error", os_path) + self.log.info("Refusing to serve hidden file or directory %r, via 404 Error", os_path) raise web.HTTPError(404, four_o_four) @@ -459,7 +459,7 @@ def get(self, path, content=True, type=None, format=None): def _save_directory(self, os_path, model, path=''): """create a directory""" if is_hidden(os_path, self.root_dir) and not self.allow_hidden: - raise web.HTTPError(400, f'Cannot create hidden directory {os_path!r}') + raise web.HTTPError(400, f'Cannot create directory {os_path!r}') if not os.path.exists(os_path): with self.perm_to_403(): os.mkdir(os_path) @@ -480,7 +480,7 @@ def save(self, model, path=''): os_path = self._get_os_path(path) if is_hidden(os_path, self.root_dir) and not self.allow_hidden: - raise web.HTTPError(400, f'Cannot create hidden file or directory {os_path!r}') + raise web.HTTPError(400, f'Cannot create file or directory {os_path!r}') self.log.debug("Saving %s", os_path) @@ -532,7 +532,7 @@ def delete_file(self, path): raise web.HTTPError(404, four_o_four) if is_hidden(os_path, self.root_dir) and not self.allow_hidden: - raise web.HTTPError(400, f'Cannot delete hidden file or directory {os_path!r}') + raise web.HTTPError(400, f'Cannot delete file or directory {os_path!r}') def is_non_empty_dir(os_path): if os.path.isdir(os_path): @@ -576,7 +576,7 @@ def rename_file(self, old_path, new_path): return if (is_hidden(old_path, self.root_dir) or is_hidden(new_path, self.root_dir)) and not self.allow_hidden: - raise web.HTTPError(400, f'Cannot rename hidden file or directory {os_path!r}') + raise web.HTTPError(400, f'Cannot rename file or directory {os_path!r}') # Perform path validation prior to converting to os-specific value since this # is still relative to root_dir. diff --git a/notebook/services/contents/handlers.py b/notebook/services/contents/handlers.py index 4c9158d182..45932448e4 100644 --- a/notebook/services/contents/handlers.py +++ b/notebook/services/contents/handlers.py @@ -129,7 +129,7 @@ def patch(self, path=''): model = self.get_json_body() old_path = model.get('path') if old_path and (cm.is_hidden(path) or cm.is_hidden(old_path)) and not cm.allow_hidden: - raise web.HTTPError(400, f'Cannot rename hidden file or directory {path!r}') + raise web.HTTPError(400, f'Cannot rename file or directory {path!r}') if model is None: raise web.HTTPError(400, 'JSON body missing') model = yield maybe_future(cm.update(model, path)) @@ -200,7 +200,7 @@ def post(self, path=''): model = self.get_json_body() copy_from = model.get('copy_from') if copy_from and (cm.is_hidden(path) or cm.is_hidden(copy_from)) and not cm.allow_hidden: - raise web.HTTPError(400, f'Cannot copy hidden file or directory {path!r}') + raise web.HTTPError(400, f'Cannot copy file or directory {path!r}') if model is not None: copy_from = model.get('copy_from') @@ -232,7 +232,7 @@ def put(self, path=''): if model.get('copy_from'): raise web.HTTPError(400, "Cannot copy with PUT, only POST") if model.get('path') and (cm.is_hidden(path) or cm.is_hidden(model.get('path'))) and not cm.allow_hidden: - raise web.HTTPError(400, f'Cannot create hidden file or directory {path!r}') + raise web.HTTPError(400, f'Cannot create file or directory {path!r}') exists = yield maybe_future(self.contents_manager.file_exists(path)) if exists: yield maybe_future(self._save(model, path)) @@ -248,7 +248,7 @@ def delete(self, path=''): cm = self.contents_manager if cm.is_hidden(path) and not cm.allow_hidden: - raise web.HTTPError(400, f'Cannot delete hidden file or directory {path!r}') + raise web.HTTPError(400, f'Cannot delete file or directory {path!r}') self.log.warning('delete %s', path) yield maybe_future(cm.delete(path)) From aee45355b568650cf650f4e2d3d551570655f844 Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Tue, 7 Jun 2022 09:39:37 -0700 Subject: [PATCH 31/78] Release 6.4.12 --- notebook/_version.py | 4 ++-- notebook/static/base/js/namespace.js | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index 678319de40..a8af5c2a9a 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,14 +5,14 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.4.12.dev0' +__version__ = '6.4.12' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' match = re.match(pattern, __version__) parts = [int(match[part]) for part in ['major', 'minor', 'patch']] if match['rest']: - parts.append(match['rest']) + parts.append(match['rest']) version_info = tuple(parts) # Downstream maintainer, when running `python.setup.py jsversion`, diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 24b2ac02e4..2ce4ad3671 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.4.11"; + Jupyter.version = "6.4.12"; Jupyter._target = '_blank'; return Jupyter; diff --git a/pyproject.toml b/pyproject.toml index 184a297c79..2c9b0be541 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.4.11" +current = "6.4.12" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From 5dbd27db0a4f0f17b818e49b8d2975db71ddb2b4 Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Tue, 7 Jun 2022 09:43:42 -0700 Subject: [PATCH 32/78] Back to dev version --- notebook/_version.py | 2 +- notebook/static/base/js/namespace.js | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notebook/_version.py b/notebook/_version.py index a8af5c2a9a..e1eeaf7718 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -5,7 +5,7 @@ import re # Version string must appear intact for tbump versioning -__version__ = '6.4.12' +__version__ = '6.4.13.dev0' # Build up version_info tuple for backwards compatibility pattern = r'(?P\d+).(?P\d+).(?P\d+)(?P.*)' diff --git a/notebook/static/base/js/namespace.js b/notebook/static/base/js/namespace.js index 2ce4ad3671..fc2f471b1e 100644 --- a/notebook/static/base/js/namespace.js +++ b/notebook/static/base/js/namespace.js @@ -73,7 +73,7 @@ define(function(){ // tree jglobal('SessionList','tree/js/sessionlist'); - Jupyter.version = "6.4.12"; + Jupyter.version = "6.4.13.dev0"; Jupyter._target = '_blank'; return Jupyter; diff --git a/pyproject.toml b/pyproject.toml index 2c9b0be541..d623560e8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ before-build-python = ["pip install babel", "npm install -g po2json"] ignore-glob = ["docs/source/examples/Notebook/Working With Markdown Cells.ipynb", "docs-translations/**/README.md", "docs/source/contributing.rst", "docs/source/examples/Notebook/JavaScript Notebook Extensions.ipynb", "CHANGELOG.md", "notebook/static/components/**/*.*"] [tool.tbump.version] -current = "6.4.12" +current = "6.4.13.dev0" regex = ''' (?P\d+)\.(?P\d+)\.(?P\d+) ((?Pa|b|rc|.dev)(?P\d+))? From 3e5b767ddbc3526729cadbc69fe472ffc3ec2e7e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 27 Jun 2022 16:39:54 -0500 Subject: [PATCH 33/78] Add changelog for 6.4.12 (#6452) * add changelog for 6.4.12 * fix changelog marker --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c477f733c..a911a90e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with +## 6.4.12 + +([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.11...6.4.12) + +- Address security advisory [GHSA-v7vq-3x77-87vg](https://github.com/jupyter/notebook/security/advisories/GHSA-v7vq-3x77-87vg) + + + ## 6.4.11 ([Full Changelog](https://github.com/jupyter/notebook/compare/6.4.10...3911672959fcd35cf4a1b1ad7c9c8a5651c17ae6)) @@ -35,8 +43,6 @@ Use `pip install pip --upgrade` to upgrade pip. Check pip version with [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ablink1073+updated%3A2022-03-15..2022-04-18&type=Issues) | [@echarles](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Aecharles+updated%3A2022-03-15..2022-04-18&type=Issues) | [@fcollonval](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Afcollonval+updated%3A2022-03-15..2022-04-18&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Agithub-actions+updated%3A2022-03-15..2022-04-18&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Ajtpio+updated%3A2022-03-15..2022-04-18&type=Issues) | [@penguinolog](https://github.com/search?q=repo%3Ajupyter%2Fnotebook+involves%3Apenguinolog+updated%3A2022-03-15..2022-04-18&type=Issues) - - ## 6.4.9 ([Full Changelog](https://github.com/jupyter/notebook/compare/v6.4.7...9e3a7001117e64a24ead07b888bd055fdd66faf3)) From 418e36b5f1ce3f2cba5dabfbfd55cfd0bbcc9c88 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Thu, 21 Jul 2022 16:22:17 -0400 Subject: [PATCH 34/78] Updated deprecated selenium find_element calls to new API. --- notebook/tests/selenium/test_dashboard_nav.py | 6 ++++-- notebook/tests/selenium/test_display_image.py | 4 +++- .../tests/selenium/test_display_isolation.py | 13 ++++++++----- notebook/tests/selenium/test_dualmode_arrows.py | 3 ++- notebook/tests/selenium/test_execute_code.py | 4 +++- notebook/tests/selenium/test_interrupt.py | 5 ++++- notebook/tests/selenium/test_kernel_menu.py | 4 ++-- notebook/tests/selenium/test_markdown.py | 5 ++++- notebook/tests/selenium/test_multiselect.py | 7 ++++++- notebook/tests/selenium/test_notifications.py | 3 ++- notebook/tests/selenium/test_prompt_numbers.py | 7 +++++-- notebook/tests/selenium/test_save.py | 8 ++++++-- notebook/tests/selenium/test_save_as_notebook.py | 6 ++++-- notebook/tests/selenium/test_save_readonly_as.py | 5 +++-- notebook/tests/selenium/utils.py | 16 ++++++++-------- setup.py | 2 +- 16 files changed, 65 insertions(+), 33 deletions(-) diff --git a/notebook/tests/selenium/test_dashboard_nav.py b/notebook/tests/selenium/test_dashboard_nav.py index f15a634578..e4f73ae08b 100644 --- a/notebook/tests/selenium/test_dashboard_nav.py +++ b/notebook/tests/selenium/test_dashboard_nav.py @@ -1,5 +1,7 @@ import os +from selenium.webdriver.common.by import By + from notebook.utils import url_path_join from notebook.tests.selenium.utils import wait_for_selector pjoin = os.path.join @@ -31,9 +33,9 @@ def get_list_items(browser): return [{ 'link': a.get_attribute('href'), - 'label': a.find_element_by_class_name('item_name').text, + 'label': a.find_element(By.CLASS_NAME, 'item_name').text, 'element': a, - } for a in browser.find_elements_by_class_name('item_link')] + } for a in browser.find_elements(By.CLASS_NAME, 'item_link')] def only_dir_links(browser): """Return only links that point at other directories in the tree""" diff --git a/notebook/tests/selenium/test_display_image.py b/notebook/tests/selenium/test_display_image.py index 4e3adfd598..65d9eef1a9 100644 --- a/notebook/tests/selenium/test_display_image.py +++ b/notebook/tests/selenium/test_display_image.py @@ -3,6 +3,8 @@ The effect of shape metadata is validated, using Image(retina=True) """ +from selenium.webdriver.common.by import By + from .utils import wait_for_tag @@ -36,7 +38,7 @@ def validate_img(notebook, cell_index, image_fmt, retina): # Find the image element that was just displayed wait_for_tag(notebook.cells[cell_index], "img", single=True) - img_element = notebook.cells[cell_index].find_element_by_tag_name("img") + img_element = notebook.cells[cell_index].find_element(By.TAG_NAME, "img") src = img_element.get_attribute("src") prefix = src.split(',')[0] diff --git a/notebook/tests/selenium/test_display_isolation.py b/notebook/tests/selenium/test_display_isolation.py index 51ca082bcd..a7266b8e3f 100644 --- a/notebook/tests/selenium/test_display_isolation.py +++ b/notebook/tests/selenium/test_display_isolation.py @@ -3,6 +3,9 @@ An object whose metadata contains an "isolated" tag must be isolated from the rest of the document. """ + +from selenium.webdriver.common.by import By + from .utils import wait_for_tag @@ -43,16 +46,16 @@ def isolated_html(notebook): iframe = wait_for_tag(notebook.browser, "iframe", single=True) # The non-isolated div will be in the body - non_isolated_div = notebook.body.find_element_by_id("non-isolated") + non_isolated_div = notebook.body.find_element(By.ID, "non-isolated") assert non_isolated_div.value_of_css_property("color") == red # The non-isolated styling will have affected the output of other cells - test_div = notebook.body.find_element_by_id("test") + test_div = notebook.body.find_element(By.ID, "test") assert test_div.value_of_css_property("color") == red # The isolated div will be in an iframe, only that element will be blue notebook.browser.switch_to.frame(iframe) - isolated_div = notebook.browser.find_element_by_id("isolated") + isolated_div = notebook.browser.find_element(By.ID, "isolated") assert isolated_div.value_of_css_property("color") == blue notebook.browser.switch_to.default_content() # Clean up the html test cells @@ -80,13 +83,13 @@ def isolated_svg(notebook): # The first rectangle will be red notebook.browser.switch_to.frame(iframes[0]) - isolated_svg_1 = notebook.browser.find_element_by_id('r1') + isolated_svg_1 = notebook.browser.find_element(By.ID, 'r1') assert isolated_svg_1.value_of_css_property("fill") == yellow notebook.browser.switch_to.default_content() # The second rectangle will be black notebook.browser.switch_to.frame(iframes[1]) - isolated_svg_2 = notebook.browser.find_element_by_id('r2') + isolated_svg_2 = notebook.browser.find_element(By.ID, 'r2') assert isolated_svg_2.value_of_css_property("fill") == black # Clean up the svg test cells diff --git a/notebook/tests/selenium/test_dualmode_arrows.py b/notebook/tests/selenium/test_dualmode_arrows.py index c881f053c6..2edbedcdda 100644 --- a/notebook/tests/selenium/test_dualmode_arrows.py +++ b/notebook/tests/selenium/test_dualmode_arrows.py @@ -1,4 +1,5 @@ """Tests arrow keys on both command and edit mode""" +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys def test_dualmode_arrows(notebook): @@ -76,7 +77,7 @@ def test_dualmode_arrows(notebook): # Tests in edit mode. # First, erase the previous content and then setup the cells to test the keys to move up. - [notebook.browser.find_element_by_class_name("fa-cut.fa").click() for i in range(6)] + [notebook.browser.find_element(By.CLASS_NAME, "fa-cut.fa").click() for i in range(6)] [notebook.body.send_keys("b") for i in range(2)] notebook.body.send_keys("a") notebook.body.send_keys(Keys.ENTER) diff --git a/notebook/tests/selenium/test_execute_code.py b/notebook/tests/selenium/test_execute_code.py index 5cf12778c3..7098d4e3ef 100644 --- a/notebook/tests/selenium/test_execute_code.py +++ b/notebook/tests/selenium/test_execute_code.py @@ -1,3 +1,4 @@ +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from .utils import shift, cmdtrl @@ -33,7 +34,8 @@ def clear_outputs(): # Execute cell with toolbar button notebook.edit_cell(index=0, content='a=13; print(a)') clear_outputs() - notebook.browser.find_element_by_css_selector( + notebook.browser.find_element( + By.CSS_SELECTOR, "button[data-jupyter-action='jupyter-notebook:run-cell-and-select-next']").click() outputs = notebook.wait_for_cell_output(0) assert outputs[0].text == '13' diff --git a/notebook/tests/selenium/test_interrupt.py b/notebook/tests/selenium/test_interrupt.py index 56fd7564ff..3d9a03998c 100644 --- a/notebook/tests/selenium/test_interrupt.py +++ b/notebook/tests/selenium/test_interrupt.py @@ -1,8 +1,11 @@ +from selenium.webdriver.common.by import By + from .utils import wait_for_selector + def interrupt_from_menu(notebook): # Click interrupt button in kernel menu - notebook.browser.find_element_by_id('kernellink').click() + notebook.browser.find_element(By.ID, 'kernellink').click() wait_for_selector(notebook.browser, '#int_kernel', single=True).click() def interrupt_from_keyboard(notebook): diff --git a/notebook/tests/selenium/test_kernel_menu.py b/notebook/tests/selenium/test_kernel_menu.py index b68b5b3db3..0f68fb2d25 100644 --- a/notebook/tests/selenium/test_kernel_menu.py +++ b/notebook/tests/selenium/test_kernel_menu.py @@ -16,7 +16,7 @@ def test_cancel_restart_or_shutdown(notebook): """Click each of the restart options, then cancel the confirmation dialog""" browser = notebook.browser - kernel_menu = browser.find_element_by_id('kernellink') + kernel_menu = browser.find_element(By.ID, 'kernellink') for menu_item in restart_selectors + [shutdown_selector]: kernel_menu.click() @@ -30,7 +30,7 @@ def test_cancel_restart_or_shutdown(notebook): def test_menu_items(notebook): browser = notebook.browser - kernel_menu = browser.find_element_by_id('kernellink') + kernel_menu = browser.find_element(By.ID, 'kernellink') for menu_item in restart_selectors: # Shutdown diff --git a/notebook/tests/selenium/test_markdown.py b/notebook/tests/selenium/test_markdown.py index cae1a7a03c..ff7f5e0033 100644 --- a/notebook/tests/selenium/test_markdown.py +++ b/notebook/tests/selenium/test_markdown.py @@ -1,8 +1,11 @@ +from selenium.webdriver.common.by import By + from nbformat.v4 import new_markdown_cell + def get_rendered_contents(nb): cl = ["text_cell", "render"] - rendered_cells = [cell.find_element_by_class_name("text_cell_render") + rendered_cells = [cell.find_element(By.CLASS_NAME, "text_cell_render") for cell in nb.cells if all([c in cell.get_attribute("class") for c in cl])] return [x.get_attribute('innerHTML').strip() diff --git a/notebook/tests/selenium/test_multiselect.py b/notebook/tests/selenium/test_multiselect.py index 6e09b25c07..a55e49d2af 100644 --- a/notebook/tests/selenium/test_multiselect.py +++ b/notebook/tests/selenium/test_multiselect.py @@ -1,5 +1,9 @@ +from selenium.webdriver.common.by import By + + INITIAL_CELLS = ['print("a")', 'print("b")', 'print("c")'] + def test_multiselect(prefill_notebook): notebook = prefill_notebook(INITIAL_CELLS) @@ -15,7 +19,8 @@ def n_selected_cells(): assert n_selected_cells() == 1 # Check that only one cell is selected according to CSS classes as well - selected_css = notebook.browser.find_elements_by_css_selector( + selected_css = notebook.browser.find_elements( + By.CSS_SELECTOR, '.cell.jupyter-soft-selected, .cell.selected') assert len(selected_css) == 1 diff --git a/notebook/tests/selenium/test_notifications.py b/notebook/tests/selenium/test_notifications.py index 8e14f4682c..fae9344856 100644 --- a/notebook/tests/selenium/test_notifications.py +++ b/notebook/tests/selenium/test_notifications.py @@ -2,6 +2,7 @@ Test the notification area and widgets """ import pytest +from selenium.webdriver.common.by import By from .utils import wait_for_selector, wait_for_script_to_return_true @@ -95,7 +96,7 @@ def test_notification(notebook): assert widget_message(notebook, "test") == "test click", "callback: message is correct" - notebook.browser.find_element_by_id("notification_test").click() + notebook.browser.find_element(By.ID, "notification_test").click() wait_for_script_to_return_true(notebook.browser, 'return IPython.notification_area.widget("test")._clicked;') wait_for_selector(notebook.browser, "#notification_test", obscures=True) diff --git a/notebook/tests/selenium/test_prompt_numbers.py b/notebook/tests/selenium/test_prompt_numbers.py index 38872b8550..b9867250f3 100755 --- a/notebook/tests/selenium/test_prompt_numbers.py +++ b/notebook/tests/selenium/test_prompt_numbers.py @@ -1,10 +1,13 @@ +from selenium.webdriver.common.by import By + + def test_prompt_numbers(prefill_notebook): notebook = prefill_notebook(['print("a")']) def get_prompt(): return ( - notebook.cells[0].find_element_by_class_name('input') - .find_element_by_class_name('input_prompt') + notebook.cells[0].find_element(By.CLASS_NAME, 'input') + .find_element(By.CLASS_NAME, 'input_prompt') .get_attribute('innerHTML').strip() ) diff --git a/notebook/tests/selenium/test_save.py b/notebook/tests/selenium/test_save.py index d566866b06..67ef1fbefe 100644 --- a/notebook/tests/selenium/test_save.py +++ b/notebook/tests/selenium/test_save.py @@ -2,8 +2,11 @@ """ from urllib.parse import quote +from selenium.webdriver.common.by import By + from .utils import wait_for_selector + promise_js = """ var done = arguments[arguments.length - 1]; %s.then( @@ -12,6 +15,7 @@ ); """ + def execute_promise(js, browser): state, data = browser.execute_async_script(promise_js % js) if state == 'success': @@ -39,7 +43,7 @@ def test_save(notebook): current_path = notebook.browser.execute_script("return Jupyter.notebook.notebook_path") assert current_path == nbname - displayed_name = notebook.browser.find_element_by_id('notebook_name').text + displayed_name = notebook.browser.find_element(By.ID, 'notebook_name').text assert displayed_name + '.ipynb' == nbname execute_promise("Jupyter.notebook.save_checkpoint()", notebook.browser) @@ -47,7 +51,7 @@ def test_save(notebook): checkpoints = notebook.browser.execute_script("return Jupyter.notebook.checkpoints") assert len(checkpoints) == 1 - notebook.browser.find_element_by_css_selector('#ipython_notebook a').click() + notebook.browser.find_element(By.CSS_SELECTOR, '#ipython_notebook a').click() hrefs_nonmatch = [] for link in wait_for_selector(notebook.browser, 'a.item_link'): href = link.get_attribute('href') diff --git a/notebook/tests/selenium/test_save_as_notebook.py b/notebook/tests/selenium/test_save_as_notebook.py index 781410ce83..70413c1291 100644 --- a/notebook/tests/selenium/test_save_as_notebook.py +++ b/notebook/tests/selenium/test_save_as_notebook.py @@ -1,11 +1,13 @@ from notebook.tests.selenium.utils import wait_for_selector +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait + def wait_for_rename(browser, nbname, timeout=10): wait = WebDriverWait(browser, timeout) def notebook_renamed(browser): - elem = browser.find_element_by_id('notebook_name') + elem = browser.find_element(By.ID, 'notebook_name') current_name = browser.execute_script('return arguments[0].innerText', elem) return current_name == nbname return wait.until(notebook_renamed) @@ -30,7 +32,7 @@ def test_save_notebook_as(notebook): # Wait for Save As modal, save save_as(notebook) wait_for_selector(notebook.browser, '.save-message') - inp = notebook.browser.find_element_by_xpath('//input[@data-testid="save-as"]') + inp = notebook.browser.find_element(By.XPATH, '//input[@data-testid="save-as"]') inp.send_keys('new_notebook.ipynb') inp.send_keys(Keys.RETURN) wait_for_rename(notebook.browser, "new_notebook") diff --git a/notebook/tests/selenium/test_save_readonly_as.py b/notebook/tests/selenium/test_save_readonly_as.py index 94c066753b..6cab7b8633 100644 --- a/notebook/tests/selenium/test_save_readonly_as.py +++ b/notebook/tests/selenium/test_save_readonly_as.py @@ -1,4 +1,5 @@ from notebook.tests.selenium.utils import wait_for_selector +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait @@ -20,7 +21,7 @@ def execute_promise(js, browser): def wait_for_rename(browser, nbname, timeout=10): wait = WebDriverWait(browser, timeout) def notebook_renamed(browser): - elem = browser.find_element_by_id('notebook_name') + elem = browser.find_element(By.ID, 'notebook_name') current_name = browser.execute_script('return arguments[0].innerText', elem) return current_name == nbname return wait.until(notebook_renamed) @@ -54,7 +55,7 @@ def test_save_readonly_notebook_as(notebook): # Wait for Save As modal, save save_as(notebook) wait_for_selector(notebook.browser, '.save-message') - inp = notebook.browser.find_element_by_xpath('//input[@data-testid="save-as"]') + inp = notebook.browser.find_element(By.XPATH, '//input[@data-testid="save-as"]') inp.send_keys('writable_notebook.ipynb') inp.send_keys(Keys.RETURN) wait_for_rename(notebook.browser, "writable_notebook") diff --git a/notebook/tests/selenium/utils.py b/notebook/tests/selenium/utils.py index d828611cfb..5041ffc157 100644 --- a/notebook/tests/selenium/utils.py +++ b/notebook/tests/selenium/utils.py @@ -133,14 +133,14 @@ def _wait_for_start(self): @property def body(self): - return self.browser.find_element_by_tag_name("body") + return self.browser.find_element(By.TAG_NAME, "body") @property def cells(self): """Gets all cells once they are visible. """ - return self.browser.find_elements_by_class_name("cell") + return self.browser.find_elements(By.CLASS_NAME, "cell") @property def current_index(self): @@ -183,10 +183,10 @@ def find_and_replace(self, index=0, find_txt='', replace_txt=''): self.to_command_mode() self.body.send_keys('f') wait_for_selector(self.browser, "#find-and-replace", single=True) - self.browser.find_element_by_id("findreplace_allcells_btn").click() - self.browser.find_element_by_id("findreplace_find_inp").send_keys(find_txt) - self.browser.find_element_by_id("findreplace_replace_inp").send_keys(replace_txt) - self.browser.find_element_by_id("findreplace_replaceall_btn").click() + self.browser.find_element(By.ID, "findreplace_allcells_btn").click() + self.browser.find_element(By.ID, "findreplace_find_inp").send_keys(find_txt) + self.browser.find_element(By.ID, "findreplace_replace_inp").send_keys(replace_txt) + self.browser.find_element(By.ID, "findreplace_replaceall_btn").click() def convert_cell_type(self, index=0, cell_type="code"): # TODO add check to see if it is already present @@ -222,10 +222,10 @@ def get_cells_contents(self): return self.browser.execute_script(JS) def get_cell_contents(self, index=0, selector='div .CodeMirror-code'): - return self.cells[index].find_element_by_css_selector(selector).text + return self.cells[index].find_element(By.CSS_SELECTOR, selector).text def get_cell_output(self, index=0, output='output_subarea'): - return self.cells[index].find_elements_by_class_name(output) + return self.cells[index].find_elements(By.CLASS_NAME, output) def wait_for_cell_output(self, index=0, timeout=10): return WebDriverWait(self.browser, timeout).until( diff --git a/setup.py b/setup.py index 7f46a7e1a5..a0296e6058 100755 --- a/setup.py +++ b/setup.py @@ -130,7 +130,7 @@ ], extras_require = { 'test': ['pytest', 'coverage', 'requests', 'testpath', - 'nbval', 'selenium', 'pytest', 'pytest-cov'], + 'nbval', 'selenium==4.3', 'pytest', 'pytest-cov'], 'docs': ['sphinx', 'nbsphinx', 'sphinxcontrib_github_alt', 'sphinx_rtd_theme', 'myst-parser'], 'test:sys_platform != "win32"': ['requests-unixsocket'], From 58b666ab2e26ee33df9cd6eaa834002bbe9c65d2 Mon Sep 17 00:00:00 2001 From: RRosio Date: Thu, 21 Jul 2022 14:16:08 -0700 Subject: [PATCH 35/78] update to working link example in docs and removed typo in contributing docs --- CONTRIBUTING.rst | 2 +- .../Distributing Jupyter Extensions as Python Packages.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6f947cb58c..b2b79e72fe 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -71,7 +71,7 @@ If you are working in development mode, you will see that your version of Jupyte Troubleshooting the Installation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you do not see that your Jupyter Notebook is not running on dev mode, it's possible that you are +If you do not see that your Jupyter Notebook is running on dev mode, it's possible that you are running other instances of Jupyter Notebook. You can try the following steps: 1. Uninstall all instances of the notebook package. These include any installations you made using diff --git a/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb b/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb index c0dd7daaec..13a2e08e6f 100644 --- a/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb +++ b/docs/source/examples/Notebook/Distributing Jupyter Extensions as Python Packages.ipynb @@ -134,7 +134,7 @@ "\n", "> Of course, in addition to the files listed, there are number of other files one needs to build a proper package. Here are some good resources:\n", "- [The Hitchhiker's Guide to Packaging](https://the-hitchhikers-guide-to-packaging.readthedocs.io/en/latest/quickstart.html)\n", - "- [Repository Structure and Python](https://kenreitz.org/essays/2013/01/27/repository-structure-and-python) by Kenneth Reitz\n", + "- [Structure of the Repository](https://docs.python-guide.org/writing/structure/) by Kenneth Reitz and Real Python\n", "\n", "> How you distribute them, too, is important:\n", "- [Packaging and Distributing Projects](https://python-packaging-user-guide.readthedocs.io/tutorials/distributing-packages/)\n", From 0ca43514cd1419d61c36ebd32c80697ee332a5bb Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Fri, 22 Jul 2022 07:51:05 -0400 Subject: [PATCH 36/78] Added blank pre-commit file. --- .pre-commit-config.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..e69de29bb2 From dc64e03afca649e6af7a780707a416a35bdf0f46 Mon Sep 17 00:00:00 2001 From: Eric Gentry Date: Mon, 25 Jul 2022 08:36:46 -0400 Subject: [PATCH 37/78] Make notebook 6.5.x point to nbclassic static assets (#6474) * Initial WIP static files removal (it builds, but has bugs). * del nbclassic from __init__. * Removed unneeded CSS/JS compilation logic. * Removed more frontend web logic. * Added nbclassic as a proper notebook dependency. * Updated nbclassic dependency version. * Added nbclassic to pyproject.toml. * Moved DEFAULT_STATIC_FILES_PATH. * Removed JS tests. * Added nbclassic tgo docs build. * Adding blank pre-commit-config for CI checks. * Edited empty pre-commit-config * Added docs to enforce-label workflow. * Revised workflow comments. * Adding WIP version_spec value to check_release step. * Changed dev version number. * Revert "Changed dev version number." This reverts commit e66673160fcd569714840c8559614ab95802e956. * Manual WIP version number update to 6.5.0.dev * Manual (WIP) version dummy value for CI check. * Revised version numbner (WIP). * Reverted version number to pre-PR value. * Pinned selenium version. * Removed setup.py jsversion call. * Removed webpack config. * Removed bower/other web files. * Disable test with modal dialog error. * Modified pip upgrade command. --- .bowerrc | 3 - .github/workflows/check-release.yml | 1 + .github/workflows/enforce-label.yml | 3 + .github/workflows/js.yml | 65 - .github/workflows/python.yml | 2 +- .pre-commit-config.yaml | 2 + MANIFEST.in | 3 - bower.json | 30 - docs/environment.yml | 1 + notebook/__init__.py | 2 - notebook/_version.py | 4 - notebook/notebookapp.py | 5 +- notebook/static/acc_overwrite.less | 61 - notebook/static/auth/css/override.css | 8 - notebook/static/auth/js/loginmain.js | 12 - notebook/static/auth/js/loginwidget.js | 38 - notebook/static/auth/js/logoutmain.js | 12 - notebook/static/auth/js/main.js | 12 - notebook/static/auth/less/login.less | 23 - notebook/static/auth/less/logout.less | 2 - notebook/static/auth/less/style.less | 7 - .../static/base/images/favicon-busy-1.ico | Bin 1150 -> 0 bytes .../static/base/images/favicon-busy-2.ico | Bin 1150 -> 0 bytes .../static/base/images/favicon-busy-3.ico | Bin 1150 -> 0 bytes notebook/static/base/images/favicon-file.ico | Bin 1150 -> 0 bytes .../static/base/images/favicon-notebook.ico | Bin 1150 -> 0 bytes .../static/base/images/favicon-terminal.ico | Bin 1150 -> 0 bytes notebook/static/base/images/favicon.ico | Bin 32038 -> 0 bytes notebook/static/base/images/logo.png | Bin 5922 -> 0 bytes notebook/static/base/js/dialog.js | 433 -- notebook/static/base/js/events.js | 37 - notebook/static/base/js/i18n.js | 16 - notebook/static/base/js/keyboard.js | 576 --- notebook/static/base/js/markdown.js | 117 - notebook/static/base/js/mathjaxutils.js | 247 -- notebook/static/base/js/namespace.js | 83 - notebook/static/base/js/notificationarea.js | 83 - notebook/static/base/js/notificationwidget.js | 168 - notebook/static/base/js/page.js | 78 - notebook/static/base/js/promises.js | 26 - notebook/static/base/js/security.js | 51 - notebook/static/base/js/utils.js | 1243 ------ notebook/static/base/less/error.less | 24 - notebook/static/base/less/flexbox.less | 269 -- notebook/static/base/less/mixins.less | 19 - notebook/static/base/less/page.less | 188 - notebook/static/base/less/style.less | 9 - notebook/static/base/less/variables.less | 62 - notebook/static/bidi/bidi.js | 45 - notebook/static/bidi/numericshaping.js | 42 - notebook/static/custom/custom.css | 8 - notebook/static/custom/custom.js | 74 - notebook/static/edit/js/editor.js | 370 -- notebook/static/edit/js/main.js | 113 - notebook/static/edit/js/menubar.js | 167 - notebook/static/edit/js/notificationarea.js | 29 - notebook/static/edit/js/savewidget.js | 192 - notebook/static/edit/less/edit.less | 55 - notebook/static/edit/less/menubar.less | 26 - notebook/static/edit/less/style.less | 7 - notebook/static/favicon.ico | Bin 32038 -> 0 bytes notebook/static/notebook/css/override.css | 7 - notebook/static/notebook/js/about.js | 50 - notebook/static/notebook/js/actions.js | 1028 ----- notebook/static/notebook/js/cell.js | 878 ----- notebook/static/notebook/js/celltoolbar.js | 470 --- .../js/celltoolbarpresets/attachments.js | 50 - .../notebook/js/celltoolbarpresets/default.js | 52 - .../notebook/js/celltoolbarpresets/example.js | 142 - .../notebook/js/celltoolbarpresets/rawcell.js | 100 - .../js/celltoolbarpresets/slideshow.js | 44 - .../notebook/js/celltoolbarpresets/tags.js | 255 -- notebook/static/notebook/js/clipboard.js | 159 - notebook/static/notebook/js/codecell.js | 620 --- .../static/notebook/js/codemirror-ipython.js | 38 - .../notebook/js/codemirror-ipythongfm.js | 63 - notebook/static/notebook/js/commandpalette.js | 207 - notebook/static/notebook/js/completer.js | 421 -- notebook/static/notebook/js/contexthint.js | 98 - notebook/static/notebook/js/kernelselector.js | 352 -- .../static/notebook/js/keyboardmanager.js | 277 -- notebook/static/notebook/js/main.js | 249 -- notebook/static/notebook/js/maintoolbar.js | 135 - notebook/static/notebook/js/mathjaxutils.js | 8 - notebook/static/notebook/js/menubar.js | 469 --- notebook/static/notebook/js/notebook.js | 3508 ----------------- .../static/notebook/js/notificationarea.js | 414 -- notebook/static/notebook/js/outputarea.js | 1129 ------ notebook/static/notebook/js/pager.js | 187 - notebook/static/notebook/js/promises.js | 22 - notebook/static/notebook/js/quickhelp.js | 349 -- notebook/static/notebook/js/savewidget.js | 228 -- notebook/static/notebook/js/scrollmanager.js | 232 -- .../static/notebook/js/searchandreplace.js | 401 -- notebook/static/notebook/js/shortcuteditor.js | 232 -- notebook/static/notebook/js/textcell.js | 654 --- notebook/static/notebook/js/toolbar.js | 153 - notebook/static/notebook/js/tooltip.js | 324 -- notebook/static/notebook/js/tour.js | 169 - notebook/static/notebook/less/ansicolors.less | 27 - notebook/static/notebook/less/cell.less | 162 - .../static/notebook/less/celltoolbar.less | 70 - notebook/static/notebook/less/codecell.less | 77 - notebook/static/notebook/less/codemirror.less | 71 - .../static/notebook/less/commandpalette.less | 69 - notebook/static/notebook/less/completer.less | 26 - .../static/notebook/less/highlight-refs.less | 5 - notebook/static/notebook/less/highlight.less | 113 - .../static/notebook/less/kernelselector.less | 12 - notebook/static/notebook/less/menubar.less | 169 - notebook/static/notebook/less/modal.less | 19 - notebook/static/notebook/less/notebook.less | 128 - .../notebook/less/notificationarea.less | 94 - .../notebook/less/notificationwidget.less | 21 - notebook/static/notebook/less/outputarea.less | 217 - notebook/static/notebook/less/pager.less | 76 - notebook/static/notebook/less/quickhelp.less | 15 - .../static/notebook/less/renderedhtml.less | 143 - notebook/static/notebook/less/savewidget.less | 62 - .../notebook/less/searchandreplace.less | 46 - notebook/static/notebook/less/style.less | 21 - .../static/notebook/less/style_noapp.less | 14 - notebook/static/notebook/less/tagbar.less | 72 - notebook/static/notebook/less/textcell.less | 83 - notebook/static/notebook/less/toolbar.less | 66 - notebook/static/notebook/less/tooltip.less | 167 - notebook/static/notebook/less/variables.less | 30 - notebook/static/robots.txt | 2 - notebook/static/services/config.js | 144 - notebook/static/services/contents.js | 268 -- notebook/static/services/kernels/comm.js | 213 - notebook/static/services/kernels/kernel.js | 1264 ------ notebook/static/services/kernels/serialize.js | 126 - notebook/static/services/sessions/session.js | 326 -- notebook/static/style/ipython.less | 12 - notebook/static/style/style.less | 37 - notebook/static/terminal/css/override.css | 7 - notebook/static/terminal/js/main.js | 63 - notebook/static/terminal/js/terminado.js | 38 - notebook/static/terminal/less/terminal.less | 39 - notebook/static/tree/js/kernellist.js | 105 - notebook/static/tree/js/main.js | 225 -- notebook/static/tree/js/newnotebook.js | 118 - notebook/static/tree/js/notebooklist.js | 1624 -------- notebook/static/tree/js/sessionlist.js | 93 - notebook/static/tree/js/shutdownbutton.js | 48 - notebook/static/tree/js/terminallist.js | 128 - notebook/static/tree/less/altuploadform.less | 37 - notebook/static/tree/less/style.less | 7 - notebook/static/tree/less/tree.less | 449 --- .../tests/selenium/test_save_readonly_as.py | 83 +- pyproject.toml | 3 +- setup.py | 25 +- setupbase.py | 381 +- webpack.config.js | 34 - 155 files changed, 69 insertions(+), 27232 deletions(-) delete mode 100644 .bowerrc delete mode 100644 .github/workflows/js.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 bower.json delete mode 100644 notebook/static/acc_overwrite.less delete mode 100644 notebook/static/auth/css/override.css delete mode 100644 notebook/static/auth/js/loginmain.js delete mode 100644 notebook/static/auth/js/loginwidget.js delete mode 100644 notebook/static/auth/js/logoutmain.js delete mode 100644 notebook/static/auth/js/main.js delete mode 100644 notebook/static/auth/less/login.less delete mode 100644 notebook/static/auth/less/logout.less delete mode 100644 notebook/static/auth/less/style.less delete mode 100644 notebook/static/base/images/favicon-busy-1.ico delete mode 100644 notebook/static/base/images/favicon-busy-2.ico delete mode 100644 notebook/static/base/images/favicon-busy-3.ico delete mode 100644 notebook/static/base/images/favicon-file.ico delete mode 100644 notebook/static/base/images/favicon-notebook.ico delete mode 100644 notebook/static/base/images/favicon-terminal.ico delete mode 100644 notebook/static/base/images/favicon.ico delete mode 100644 notebook/static/base/images/logo.png delete mode 100644 notebook/static/base/js/dialog.js delete mode 100644 notebook/static/base/js/events.js delete mode 100644 notebook/static/base/js/i18n.js delete mode 100644 notebook/static/base/js/keyboard.js delete mode 100644 notebook/static/base/js/markdown.js delete mode 100644 notebook/static/base/js/mathjaxutils.js delete mode 100644 notebook/static/base/js/namespace.js delete mode 100644 notebook/static/base/js/notificationarea.js delete mode 100644 notebook/static/base/js/notificationwidget.js delete mode 100644 notebook/static/base/js/page.js delete mode 100644 notebook/static/base/js/promises.js delete mode 100644 notebook/static/base/js/security.js delete mode 100644 notebook/static/base/js/utils.js delete mode 100644 notebook/static/base/less/error.less delete mode 100644 notebook/static/base/less/flexbox.less delete mode 100644 notebook/static/base/less/mixins.less delete mode 100644 notebook/static/base/less/page.less delete mode 100644 notebook/static/base/less/style.less delete mode 100644 notebook/static/base/less/variables.less delete mode 100644 notebook/static/bidi/bidi.js delete mode 100644 notebook/static/bidi/numericshaping.js delete mode 100644 notebook/static/custom/custom.css delete mode 100644 notebook/static/custom/custom.js delete mode 100644 notebook/static/edit/js/editor.js delete mode 100644 notebook/static/edit/js/main.js delete mode 100644 notebook/static/edit/js/menubar.js delete mode 100644 notebook/static/edit/js/notificationarea.js delete mode 100644 notebook/static/edit/js/savewidget.js delete mode 100644 notebook/static/edit/less/edit.less delete mode 100644 notebook/static/edit/less/menubar.less delete mode 100644 notebook/static/edit/less/style.less delete mode 100644 notebook/static/favicon.ico delete mode 100644 notebook/static/notebook/css/override.css delete mode 100644 notebook/static/notebook/js/about.js delete mode 100644 notebook/static/notebook/js/actions.js delete mode 100644 notebook/static/notebook/js/cell.js delete mode 100644 notebook/static/notebook/js/celltoolbar.js delete mode 100644 notebook/static/notebook/js/celltoolbarpresets/attachments.js delete mode 100644 notebook/static/notebook/js/celltoolbarpresets/default.js delete mode 100644 notebook/static/notebook/js/celltoolbarpresets/example.js delete mode 100644 notebook/static/notebook/js/celltoolbarpresets/rawcell.js delete mode 100644 notebook/static/notebook/js/celltoolbarpresets/slideshow.js delete mode 100644 notebook/static/notebook/js/celltoolbarpresets/tags.js delete mode 100644 notebook/static/notebook/js/clipboard.js delete mode 100644 notebook/static/notebook/js/codecell.js delete mode 100644 notebook/static/notebook/js/codemirror-ipython.js delete mode 100644 notebook/static/notebook/js/codemirror-ipythongfm.js delete mode 100644 notebook/static/notebook/js/commandpalette.js delete mode 100644 notebook/static/notebook/js/completer.js delete mode 100644 notebook/static/notebook/js/contexthint.js delete mode 100644 notebook/static/notebook/js/kernelselector.js delete mode 100644 notebook/static/notebook/js/keyboardmanager.js delete mode 100644 notebook/static/notebook/js/main.js delete mode 100644 notebook/static/notebook/js/maintoolbar.js delete mode 100644 notebook/static/notebook/js/mathjaxutils.js delete mode 100644 notebook/static/notebook/js/menubar.js delete mode 100644 notebook/static/notebook/js/notebook.js delete mode 100644 notebook/static/notebook/js/notificationarea.js delete mode 100644 notebook/static/notebook/js/outputarea.js delete mode 100644 notebook/static/notebook/js/pager.js delete mode 100644 notebook/static/notebook/js/promises.js delete mode 100644 notebook/static/notebook/js/quickhelp.js delete mode 100644 notebook/static/notebook/js/savewidget.js delete mode 100644 notebook/static/notebook/js/scrollmanager.js delete mode 100644 notebook/static/notebook/js/searchandreplace.js delete mode 100644 notebook/static/notebook/js/shortcuteditor.js delete mode 100644 notebook/static/notebook/js/textcell.js delete mode 100644 notebook/static/notebook/js/toolbar.js delete mode 100644 notebook/static/notebook/js/tooltip.js delete mode 100644 notebook/static/notebook/js/tour.js delete mode 100644 notebook/static/notebook/less/ansicolors.less delete mode 100644 notebook/static/notebook/less/cell.less delete mode 100644 notebook/static/notebook/less/celltoolbar.less delete mode 100644 notebook/static/notebook/less/codecell.less delete mode 100644 notebook/static/notebook/less/codemirror.less delete mode 100644 notebook/static/notebook/less/commandpalette.less delete mode 100644 notebook/static/notebook/less/completer.less delete mode 100644 notebook/static/notebook/less/highlight-refs.less delete mode 100644 notebook/static/notebook/less/highlight.less delete mode 100644 notebook/static/notebook/less/kernelselector.less delete mode 100644 notebook/static/notebook/less/menubar.less delete mode 100644 notebook/static/notebook/less/modal.less delete mode 100644 notebook/static/notebook/less/notebook.less delete mode 100644 notebook/static/notebook/less/notificationarea.less delete mode 100644 notebook/static/notebook/less/notificationwidget.less delete mode 100644 notebook/static/notebook/less/outputarea.less delete mode 100644 notebook/static/notebook/less/pager.less delete mode 100644 notebook/static/notebook/less/quickhelp.less delete mode 100644 notebook/static/notebook/less/renderedhtml.less delete mode 100644 notebook/static/notebook/less/savewidget.less delete mode 100644 notebook/static/notebook/less/searchandreplace.less delete mode 100644 notebook/static/notebook/less/style.less delete mode 100644 notebook/static/notebook/less/style_noapp.less delete mode 100644 notebook/static/notebook/less/tagbar.less delete mode 100644 notebook/static/notebook/less/textcell.less delete mode 100644 notebook/static/notebook/less/toolbar.less delete mode 100644 notebook/static/notebook/less/tooltip.less delete mode 100644 notebook/static/notebook/less/variables.less delete mode 100644 notebook/static/robots.txt delete mode 100644 notebook/static/services/config.js delete mode 100644 notebook/static/services/contents.js delete mode 100644 notebook/static/services/kernels/comm.js delete mode 100644 notebook/static/services/kernels/kernel.js delete mode 100644 notebook/static/services/kernels/serialize.js delete mode 100644 notebook/static/services/sessions/session.js delete mode 100644 notebook/static/style/ipython.less delete mode 100644 notebook/static/style/style.less delete mode 100644 notebook/static/terminal/css/override.css delete mode 100644 notebook/static/terminal/js/main.js delete mode 100644 notebook/static/terminal/js/terminado.js delete mode 100644 notebook/static/terminal/less/terminal.less delete mode 100644 notebook/static/tree/js/kernellist.js delete mode 100644 notebook/static/tree/js/main.js delete mode 100644 notebook/static/tree/js/newnotebook.js delete mode 100644 notebook/static/tree/js/notebooklist.js delete mode 100644 notebook/static/tree/js/sessionlist.js delete mode 100644 notebook/static/tree/js/shutdownbutton.js delete mode 100644 notebook/static/tree/js/terminallist.js delete mode 100644 notebook/static/tree/less/altuploadform.less delete mode 100644 notebook/static/tree/less/style.less delete mode 100644 notebook/static/tree/less/tree.less delete mode 100644 webpack.config.js diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index b1c953d5ac..0000000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "notebook/static/components" -} \ No newline at end of file diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index 14d6186a42..870668b4ef 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -61,6 +61,7 @@ jobs: uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v1 with: token: ${{ secrets.GITHUB_TOKEN }} + version_spec: 100.100.100 - name: Check Links if: ${{ matrix.group == 'link_check' }} uses: jupyter-server/jupyter_releaser/.github/actions/check-links@v1 diff --git a/.github/workflows/enforce-label.yml b/.github/workflows/enforce-label.yml index 354a0468d7..d09f468975 100644 --- a/.github/workflows/enforce-label.yml +++ b/.github/workflows/enforce-label.yml @@ -1,3 +1,6 @@ +# Pull requests to this repo should have a triage label, +# this workflow ensures that this is the case. + name: Enforce PR label on: diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml deleted file mode 100644 index 6f1bf4c383..0000000000 --- a/.github/workflows/js.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Linux JS Tests - -on: - push: - branches: '*' - pull_request: - branches: '*' - -jobs: - build: - runs-on: ${{ matrix.os }}-latest - strategy: - fail-fast: false - matrix: - os: [ubuntu, macos] - group: [notebook, base, services] - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Set up Node - uses: actions/setup-node@v1 - with: - node-version: '12.x' - - - name: Cache node modules - uses: actions/cache@v2 - env: - cache-name: cache-node-modules - with: - # npm cache files are stored in `~/.npm` on Linux/macOS - path: ~/.npm - key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-build-${{ env.cache-name }}- - ${{ runner.os }}-build- - ${{ runner.os }}- - - - name: Cache pip on Linux - uses: actions/cache@v1 - if: startsWith(runner.os, 'Linux') - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ matrix.python }}-${{ hashFiles('**/requirements.txt', 'setup.py') }} - restore-keys: | - ${{ runner.os }}-pip-${{ matrix.python }} - - - name: Temporary workaround for sanitizer loading in JS Tests - run: | - cp tools/security_deprecated.js notebook/static/base/js/security.js - - - name: Install dependencies - run: | - pip install --upgrade pip - pip install --upgrade setuptools wheel - npm install - npm install -g casperjs@1.1.3 phantomjs-prebuilt@2.1.7 - pip install .[test] - - - name: Run Tests - run: | - python -m notebook.jstest ${{ matrix.group }} diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 282ea1fd85..77fc5dfe6c 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -22,7 +22,7 @@ jobs: architecture: 'x64' - name: Upgrade packaging dependencies run: | - pip install --upgrade pip setuptools wheel --user + python -m pip install --upgrade pip setuptools wheel --user - name: Get pip cache dir id: pip-cache run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..0952f479dc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,2 @@ +repos: [] + diff --git a/MANIFEST.in b/MANIFEST.in index c28b820105..64b17e3a88 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,13 +3,10 @@ include CONTRIBUTING.rst include README.md include CHANGELOG.md include package.json -include bower.json -include .bowerrc include pyproject.toml include setup.py include setupbase.py include Dockerfile -include *.js graft tools graft notebook/tests diff --git a/bower.json b/bower.json deleted file mode 100644 index 5f72f6ff04..0000000000 --- a/bower.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "jupyter-notebook-deps", - "version": "0.0.1", - "dependencies": { - "backbone": "components/backbone#~1.2", - "bootstrap": "bootstrap#~3.4", - "bootstrap-tour": "0.9.0", - "codemirror": "components/codemirror#5.56.0+components1", - "create-react-class": "https://cdn.jsdelivr.net/npm/create-react-class@15.6.3/create-react-class.min.js", - "es6-promise": "~1.0", - "font-awesome": "components/font-awesome#~4.7.0", - "google-caja": "5669", - "jed": "~1.1.1", - "jquery": "components/jquery#~3.5.0", - "jquery-typeahead": "~2.10.6", - "jquery-ui": "components/jqueryui#~1.12", - "marked": "~0.7", - "MathJax": "^2.7.4", - "moment": "~2.19.3", - "react": "~16.0.0", - "requirejs": "~2.2", - "requirejs-text": "~2.0.15", - "requirejs-plugins": "~1.0.3", - "text-encoding": "~0.1", - "underscore": "components/underscore#~1.8.3", - "xterm.js": "https://unpkg.com/xterm@~3.1.0/dist/xterm.js", - "xterm.js-css": "https://unpkg.com/xterm@~3.1.0/dist/xterm.css", - "xterm.js-fit": "https://unpkg.com/xterm@~3.1.0/dist/addons/fit/fit.js" - } -} diff --git a/docs/environment.yml b/docs/environment.yml index 55cea9b26c..8df81e98aa 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -13,6 +13,7 @@ dependencies: - sphinx - terminado - myst-parser +- nbclassic>=0.4.0 - pip: - nbsphinx - Send2Trash diff --git a/notebook/__init__.py b/notebook/__init__.py index c72096f26b..6c71e6e7b6 100644 --- a/notebook/__init__.py +++ b/notebook/__init__.py @@ -1,8 +1,6 @@ """The Jupyter HTML Notebook""" import os -# Packagers: modify this line if you store the notebook static files elsewhere -DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static") # Packagers: modify the next line if you store the notebook template files # elsewhere diff --git a/notebook/_version.py b/notebook/_version.py index e1eeaf7718..95e5289c0e 100644 --- a/notebook/_version.py +++ b/notebook/_version.py @@ -14,7 +14,3 @@ if match['rest']: parts.append(match['rest']) version_info = tuple(parts) - -# Downstream maintainer, when running `python.setup.py jsversion`, -# the version string is propagated to the JavaScript files, do not forget to -# patch the JavaScript files in `.postN` release done by distributions. diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index a274f4bb38..bd82f497ca 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -66,11 +66,12 @@ from notebook import ( DEFAULT_NOTEBOOK_PORT, - DEFAULT_STATIC_FILES_PATH, DEFAULT_TEMPLATE_PATH_LIST, __version__, ) - +import nbclassic +# Packagers: modify this line if you store the notebook static files elsewhere +DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(nbclassic.__file__), "static") from .base.handlers import Template404, RedirectWithParams from .log import log_request diff --git a/notebook/static/acc_overwrite.less b/notebook/static/acc_overwrite.less deleted file mode 100644 index 4209ea633b..0000000000 --- a/notebook/static/acc_overwrite.less +++ /dev/null @@ -1,61 +0,0 @@ -//This is file created for overwriting some of bootstrap element color in order to satisfiy the color contrast greater than 4.5:1. - -.btn-danger{ - color: #ffffff; - background-color: #df0404; - border-color: #df0404; -} - -.btn-warning{ - color: #ffffff; - background-color:#b46102; - border-color: #b46102; -} - -@link-color: #296eaa; - -.close { - float: right; - font-size: (@font-size-base * 1.5); - font-weight: @close-font-weight; - line-height: 1; - color: @close-color; - text-shadow: @close-text-shadow; - .opacity(.6); - - &:hover, - &:focus { - color: @close-color; - text-decoration: none; - cursor: pointer; - .opacity(1.0); -} button& { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; - appearance: none; - } -} - -.navbar-nav { - > li > a { - color: @navbar-default-link-color; - // To make keyboard focus clearly visible on the Controls(File,Edit,View,Insert,Cell...etc)present on the menubar. - //&:hover, //To use browser's hover default value - &:focus { - /* -webkit-focus-ring-color = '#5B9DD9' */ - outline: -webkit-focus-ring-color auto 5px; - // color: @navbar-default-link-hover-color; - // background-color: @navbar-default-link-hover-bg; - } - } -} - -.menu_focus_highlight{ - a:focus { - outline: -webkit-focus-ring-color auto 5px; -} -} - diff --git a/notebook/static/auth/css/override.css b/notebook/static/auth/css/override.css deleted file mode 100644 index 0cb116ec2c..0000000000 --- a/notebook/static/auth/css/override.css +++ /dev/null @@ -1,8 +0,0 @@ -/*This file contains any manual css for this page that needs to override the global styles. -This is only required when different pages style the same element differently. This is just -a hack to deal with our current css styles and no new styling should be added in this file.*/ - -#ipython-main-app { - padding-top: 50px; - text-align: center; -} diff --git a/notebook/static/auth/js/loginmain.js b/notebook/static/auth/js/loginmain.js deleted file mode 100644 index 0c6b661f78..0000000000 --- a/notebook/static/auth/js/loginmain.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -define(['jquery', 'base/js/namespace', 'base/js/page'], function($, IPython, page) { - function login_main() { - var page_instance = new page.Page('div#header', 'div#site'); - page_instance.show(); - $('input#password_input').focus(); - IPython.page = page_instance; - } - return login_main; -}); diff --git a/notebook/static/auth/js/loginwidget.js b/notebook/static/auth/js/loginwidget.js deleted file mode 100644 index 8b68d3e84b..0000000000 --- a/notebook/static/auth/js/loginwidget.js +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -define([ - 'jquery', - 'base/js/utils', -], function($, utils){ - "use strict"; - - var LoginWidget = function (selector, options) { - options = options || {}; - this.base_url = options.base_url || utils.get_body_data("baseUrl"); - this.selector = selector; - if (this.selector !== undefined) { - this.element = $(selector); - this.bind_events(); - } - }; - - - LoginWidget.prototype.bind_events = function () { - var that = this; - this.element.find("button#logout").click(function () { - window.location = utils.url_path_join( - that.base_url, - "logout" - ); - }); - this.element.find("button#login").click(function () { - window.location = utils.url_path_join( - that.base_url, - "login" - ); - }); - }; - - return {'LoginWidget': LoginWidget}; -}); diff --git a/notebook/static/auth/js/logoutmain.js b/notebook/static/auth/js/logoutmain.js deleted file mode 100644 index 41e775053b..0000000000 --- a/notebook/static/auth/js/logoutmain.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -define(['base/js/namespace', 'base/js/page'], function(IPython, page) { - function logout_main() { - var page_instance = new page.Page('div#header', 'div#site'); - page_instance.show(); - - IPython.page = page_instance; - } - return logout_main; -}); diff --git a/notebook/static/auth/js/main.js b/notebook/static/auth/js/main.js deleted file mode 100644 index 68d7871c58..0000000000 --- a/notebook/static/auth/js/main.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -define(['./loginmain', './logoutmain', 'bidi/bidi'], function (login_main, logout_main, bidi) { - if(bidi.isMirroringEnabled()){ - $("body").attr("dir","rtl"); - } - return { - login_main: login_main, - logout_main: logout_main - }; -}); diff --git a/notebook/static/auth/less/login.less b/notebook/static/auth/less/login.less deleted file mode 100644 index 8a4ce3ffcb..0000000000 --- a/notebook/static/auth/less/login.less +++ /dev/null @@ -1,23 +0,0 @@ -// Custom styles for login.html -.center-nav { - display: inline-block; - // pull the lower margin back - margin-bottom: -4px; -} - -[dir="rtl"] .center-nav { - form.pull-left { - .pull-right(); - } - .navbar-text { - float: right; - } -} - -[dir="rtl"] .navbar-inner { - text-align: right; -} - -[dir="rtl"] div.text-left { - .text-right(); -} diff --git a/notebook/static/auth/less/logout.less b/notebook/static/auth/less/logout.less deleted file mode 100644 index 63cd701c9a..0000000000 --- a/notebook/static/auth/less/logout.less +++ /dev/null @@ -1,2 +0,0 @@ -// Custom styles for logout.html - diff --git a/notebook/static/auth/less/style.less b/notebook/static/auth/less/style.less deleted file mode 100644 index 4d1919cc03..0000000000 --- a/notebook/static/auth/less/style.less +++ /dev/null @@ -1,7 +0,0 @@ -/*! -* -* IPython auth -* -*/ -@import "login.less"; -@import "logout.less"; \ No newline at end of file diff --git a/notebook/static/base/images/favicon-busy-1.ico b/notebook/static/base/images/favicon-busy-1.ico deleted file mode 100644 index 5b46a822610b0f49bad2579cbd0ff8bfbb80d0fd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmb_byH3L}6g^dGo28;tXBH$Rx;Ed$575>80V^QE&SR*=53qHun5k4Lm9n-QOtR82Nz_X^HRP`gV>i zs3~bABf?&*Z>V1;{4?rb3H^!sbwXcIe@y5(^@12TVw5woBl3p!pD=RAn0I1S(MEUf!g|U7v-&*>0|Gw9aAF6di9sM_PubX2Yvi@>L zG=DBG)~52O^%L_sK2<+@2kN}}8#OYZ9`u)Ep9UFL3Y z=FNL|``!U5p2Z?~Eu)nIN&u<^DKduV4WL$`DERxK^aD!kNZg(E<8rvYl2GY=ME#w! zw(*bYUlUyqR^&$kf0O!|$p6F$r}T+v{%D_s(tXA(h+IO;+Hiw}^DUIlGIm023Hu!z zu4hEnXG?hFpECBlm-9%!y#Ke?J`a;Ru7&K4-)nO;T-Fz-wDhkN(A(5Q{ge5r1^jjD yq4|-2z`w_)kM8-6n|-?n?i}71_Y3S4mTBO?X|p)4fLjl^7Tyr;a(P2M;(Y>vrxNA> diff --git a/notebook/static/base/images/favicon-busy-3.ico b/notebook/static/base/images/favicon-busy-3.ico deleted file mode 100644 index b5edce573303a1d750106afa0a3b17bb2e6f2fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmb_bv1$TA5PiXDk|Jo8+9n_Y3x)Vi`2fLkKEXB-v{wTWzrfB?k~)Dv1Y6U^YVwA4 zPh|JbExhC1&b>Ep=D3{&E_l~!;IodWOW+PbgCIr5@mv6E_Y@6hzeINs)kOS7`=`~2 zd##}8S3>{KXm95qCHRL0{!W6wlk0ci@XQ)-L^;c|*l{lzmludum^&s`M4M>s<0;ol zo-N~@f5O}k!u*DJ;+078+xO3pa$U_Cxk1(^7KqyaTNAf|Y@E{`1I-hnwtrp!8myu{ z>P7o!^Yak+JR#boUNk>;2CjX2u!`PLkI!$~?ECW-uMspnP4L8GQ3AfNj`7zbAWlw`wvCu45I)5 diff --git a/notebook/static/base/images/favicon-file.ico b/notebook/static/base/images/favicon-file.ico deleted file mode 100644 index 8167018cd005ff4a24d8287c620539e23c69aac9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmeHGu?@m75Ih7usc1=q#Ar}3g9{1u5G9Vze%<}~9qSED z_*E4+*VxQ}1%Nd{icIFY0Mu3#9X}t*S|qcPXdmjkckEEPihH)7%2nLouWvxzmEyD; z_Z}H)ul%aFKJ%B}O15MDW@wjZiljJOvMuwx&zB)}Yvgo9FK{i_b#L8MN~r!Z`{GSC cvToVotH@v7c?w*vZ?LC+B96p?o;l#&8_znXx&QzG diff --git a/notebook/static/base/images/favicon-notebook.ico b/notebook/static/base/images/favicon-notebook.ico deleted file mode 100644 index 4537e2d989843ae1a96a0548aa0a7066a22e2698..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmd6kJxT;Y5QX2WpqU$)xPggaBK}}1>McBhsCWizK|@m`1#e)g;lhS0Vx(Z8XsF1B zf@t|QGc63$HWTg)-xU2`y`G|5YVo$);ya`5F=+Oz;Y!U7Un(3%^tjC*nw^$zt$lCLQe;7 z68RSTXM;T!>A8mbS(zU-$nVo*u$!1+qs)I$$1A@12wJ$KD_DZdiXRr{6L1H)bNs;% z_xMoD7kliL`IDM&zEXNy)YY0_vYhz#zuJz@P!dKp~(AL>x#lFaYILfs!CRtU&#LehmNA|NqHQ z|NlP@`~yXO{r??6b?qo(q;Ts0O91s>q0kHwp#F0dnjr+#{|<-?NpU`0ZTkWVxAGzYSw`z#PH=!+V$+80G`{{Xl#ih#x~Shz$}0 J$-&gZXaKG5Ui|<7 diff --git a/notebook/static/base/images/favicon.ico b/notebook/static/base/images/favicon.ico deleted file mode 100644 index 2d1bcff7ca0ed6c77fdf1b016986f9137a43d31a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32038 zcmeI4U5FG{7ROsAA|fG?5JY4_k?9XRYJ%itpLE5Qd5}CTSqV$nU6$YuB0hvnSGNw% z5*9HcG7rljAx|Of2kb(SAVHR88S)UqAR>zoOCCg)ARz>&t7k?;dw>7xx;=NQtGcVF zKWDlc&eW}2_nxnN>fUqDJvFJ+medQWmtRit+nf6R3#ru4RoYv-{|oj1pzih8{rmA$ z>X)ykQb&*0?z2BjrJnyZmCAA}6nlX!-e20#FZiogt6PGQF1WkXh4N{j-~(lMcd%XX zp0?w_-+`wNviJo^c=^??1z);#+$d~?5U`?YYa`~5LEYVy==z5f1&y%TDiN>!_!faaOK zz)`@=Gc(@YMTPaV`5WePw!nPgxXgFuS>S8nAt2ijsI0nKwNcw{$&x9S_k(v1u;)56 z=S5xBc20fQ%SXQWMDyQeKsFskr&YcBUKXx)_{ccree}EA0o@>9cZD+!56sA4(YRvcV_c;7|FUkpY1j9noOLR1dbFTv@I&_u>sHrgHu!^A=)D5GXX^9L zO2c@VXT7=(f@ZH|N$@uA6%w5!I*DrogP03!nwkqWC&uNs({`Hcp?AjMG3$@k22We+K#J_PrGf7)ViZ3WZF61JvUb#7=|1Zp*z<@#RZPFS3Rl1{Bx& zE%V~J@q$|vaDPs9dZspNI}CF2K5U1t6YzSC>pY-+?sveCfZ{h=_XH}dPS4b4KJAp% z=Y(+dQvQYSH6& zyudZ0JU&&EPkcv}(^KH8;k+Um-IjqhXFEE2CtX^A5LTJ9&JynXT3Z)210ufdf^#%=BL9M%57^x`)52zg|A zS2TQDPi?#McxoX7d!23Y_5p3po<#b8m?@Osr~YuJu=G+C58bNIK?B_!gzkCdu-BKe zcoUCNw9-@V1pTx|J_Qz7BhR9jp7R{ZPsx3eF`0yyZ#M+T)x0O_Vxqw z)(zhKd|10d)WKH{8<^v&_hUXiQ*Brq;P({IIB#+%50tvUPw!OjytA|u`#B>X#jA}k zH@&=*_jWE_baQt7FqWA=a_IO^;@R*=d}hDie{(iB!(_YKozlc|}f7BjLd$nQXKj$b13N3ho?lW72VNEc;`Pz-6VHZ-4KYT+_?2Qv=h<@} z%oQs=j|C}4hJ7n;{60WDq!w$X?8mQ`BI8${3{J(1AKEcU`f5BB^ZXe3-bI$&f$4Hn zJi-+h6pQ4&%PTes4aMs3`!>M2NIeGoX}VB}6$8yWWy!E$%V3pdgT$E^ZxOGXhUUp^ zF^?r2CLKbbVZT=9LvQ zMXuKIbK0n_>F48az^8iAZaaQ184i84dVXQ)-lruUSu(XMw|egBJQSaIesS<>`{x(+ zafm!W3e{J*PW>}7Pv>#UT{u57{Oh`Ue#hBb!8>E~&KS(N=o(e7@f@n#hA|}G|AhAb z`Ca#OXLBWUe!BQAHvh~!H}&6*sIteuC*SIL1>17}jCSBGUl9#kcik@6&#!BF0p9sS zOa1#Y8d(BYaBvphhx6;8E||ADGjLnx4k3vA(kJc(~tW# z`5RNU{0&d9-Z;7EC=Xr!MnCk@ibgFTNZ&;1mt6a;2jyuU1!e$~rTnr8g+GKrn&>a`&h=QT)?OPD|7olnKSR;BkN3d*@Ig7j-GjKXsbTzyAgHjXGc09z{zu2mOBJO67NWe|e94FLO8z_n~;`w=J-5$`k*Y59etBx_}q))DJym zi%Y*C_SQFBkE>(xB;I&iRiR>fntb>PkIPWkh=V=?G<=-~5itr16V zRQ3NcZmnM?d>n@upyS?zuQ`02t7)fliP+)?d}}o+P+4`*xvCgF*PGZGaYOk>>zhx+ zvv}{+7QmzY+PF6VBl6usCgP{%jK=MfdyCA)l}uRQe%5hb zIxbyccXm$h8he#2?eIh{?>_J;HWk3*E-=o&Iy%$&d({pSNB%5S3_4ZZ%63_of9>j!QznRL|!36W;!!X}4TdcfCF2#w> zByX53l2Nh-pJ^Qp)@|1NQId7Dj$tBKW2`;VNura)1`-=cY#_0L#0C-@NNganfy4$9 z8%S&*v4O+}n%lrA#e~9S9rgI97yl+UtQhfTeODCQSByU_{wIy>6vw(wfX-G$t@1_N&h7y__Q}eEax-{u^-b>Dyp1;VA3Pm2 z$$vqY572S(wTmnJ2W<<=!NIPlb*>qd+uXOC|6*iP?vTL|d3D@7%D+)glIhp$t8%Th zMlsfRBl+@eKr(c~4>V%uztwUr+%LR*By!f2yVeP9^;HgCEuRH>$*n`)zU9A(epm7z z$z?>3H~rkk==?`=BbjICSg&r@=C8r$KS~Gkm*8t8^IvWIMF%69|7`1El=I(>9!B~5 z2crXBNA>#`TL**q{)x5W@i2VPy0+(|@1RCodHoeOwWMV5f??M@&`cNCPTx&orSL51;w;wYn|jKdH`VMJkJzxe(J zS6$a6JR~D92_rZN?tsEbL;)93)Nyc}on2u-QDy*95h9?x#UX-_H%a>Lf7-q`b#J;m z9nvAe-1@$osycP*)TwjpRGm6ieWPnlC}d}6CzY0#rfZtsU(<|Tx~|X5&CNa9gjjT0 zaV21rfNsk*VrI^qIV}$yII!9_z6etkTE_E{ZeO~`jc(@C7CD8a1Xb=T5p6cf_o*tN7m+pBy z+V1f9#-8wS7wSr&@gQ_ zzNTw!iVDh-G_6c4%IsdE8Crp%8@t(Ty{~(;wJB%n>-3ELLT4K|3|_!-@DT8G%ramn z48uFa%)7BGfu<$_{jtX$YgbWOxf#ZS1DAn5oA5|9MoY7LTw7%r51<^w4b4c5hU+YY zekn@!-yUz`yp-u%H>l+kCr<2CQBgHj*R=sC(^npkXL?Rf&UzKcO}i3kN)pf?dE}7^ z0mGQCFjbM9l+-?)`?xf#*ELl})yv8@(ed~yPpAP_GmmB%4;Gb{ZYay@(amhvjel4Y_~esM z+A@Y-r&kZ~T=+xjorgWfw+UWvf6eP1q9-I=^~~>n_o&&qlFXjB8d}vl2Kl)rtl?tg zq6)+KuxLWJ>l>~Om$EB?_(~w?g_}8ZW-(;nRY@0Tb-N{CRILcE5Nd%Vt!w%rjM$x+ zvb!|H^DQP>F;0LY91LRI`Y89~5NJdCLftUB61Q`ZsLmQjYb~I!D$MM0Q)=$^)zt-V z%9TLVlt55{oUD~(^|~Sus9efmuW?R@`8!O8v^|`9$w9mGwWp3J)KvR2;!*VsZ`r&h_uMu^d8nwpu&u9 zuhHo6V8b{USZQ*6U3^RQ`!>d{qv1MbWM7w5QhsPG%w_U){vJnvE_Fu!{o(NI3%li( zTONmo800Se|>*Ax*{;}r>bSZe*e~6Z`lTfTCS$k($ek({~{AcTWj8UC+3m)AxWeH`gAJ$vxBw?)TyQI5K?NtVNlelTL!x zwY1lb`0w=fDw_#coY6fkpc%tanqm$+r~111pKfTyF&WgeSC^4~4n=ygsQgft9tdP$ z%H3O<(e35r8DFe3@!}ODgsD83i{A&*(#C!>uf`)sj`UDIQH3GsmE>bmAn@+ux!J4_ zttUyZHuGWP+rb%Z!f9Xdi$;wa)xM;p~P}5iP_W_`nCWp6@Y9DdNIk|g&h8V+ikbq@!D&z$>b6>pcG@^`I->Y zVd9_yp*-^bp5yT;{g8I5_c+G#5ne;rW@D~x_9kk5{ki!wPL=WqBW4gqO3m3mCDD`E zA3wwwRhs^cS=Nl=Qb%9?yu?e3i;F+z1%^`|2fCI3UdG=coI_Ik7Y58B(z&yo(mN{EL_n)O<9vENC)uB_|d}B9f zQy>ORCtj$mcpZc>(kFOUk(3qW0n)!A{)ERC7!WsbV7bf&3}rA+#MBWbvI6*{txPQm zlx3uf0uS3T0XH~cTY>*g9^fCUWs(NI)+|?Juh(1U#Fc!=Wcy$EJXC&=g z%U00oOP`fNID@LzQC*k?zFY_CEYn%&(5x4 zlSZoC&~UkQYGn5wUQ!;s0kNi`+iODfZDtoQ@g;ccB>eG`|CErB@CYW!4hO#zdEvu= zE#zsdl%+BIj$?4=DX?G?S_`0^uD~yl{T2I$_Z3*!B=tt91<=Y<^O9zdV3}y)oCxjP zx4%A?y^hej_uhLGzxn2yB_vx*UIL2p#LAT`r_r`*A0DIVbNZYIkFp>9vEUM)gGrZ5 za;)Sd1WWGVCqnljVza$(sFR(TnE1ijXP>?E@ZrN8&xA}kko1)P{T4JOW{8WWA z$Z9p?&{LcF28}8PvnY9k1(fG;`eB)jv^k$ifs4mB<+0kQ?7Nhdl-mHPvKW2qt+#fw zkZLAuDD|KSAkBP~3k?SA+mVF%twlsEn8LquBF(}@Zcm6hd`v@)B%yhVH9x`3iEBa zNwWw74K*_n zVw(cNZ(anvxPaNm$+1d%lNfz@23d%gtk!_J`v(V-n7LsCbZcy1fK5gbZL(A=y9%UA zqlu&$kg82DTotficr#Y^7+j^i_&E-Ie)Nc+)e)91T`DHd3Kh&OJx0x|>5vK3Pk|ln z)Tz^3N_tiXQm}4f7b1{-`<7fmnuAz7BkNBz|r2#D9YL4%oYZjZS?_nS(LM; zFiq7R1n+~)TC`Wn`PRqWIH=3yFDr-~gi1)*zkmM)#M^?o=zlhd85j-4RiJ3nX(D9| zpj~xk1=HnA&OdC`$hQShLxI>F{|y)M7qk=HU=t^zD00s7dm8~Vl-U%B{30h#JoDoG zVE-0$q6l5uOWw0*PZ`fiH$&e{&stO(w6!}b0)=(a;@W7rM%qA?O=wX1cXU<Jx~nN1gIq3{)edFi80AVYNeuY@Odz=;XhJ z+6sq(5fw=AC||%qs-+}=lyMewA?{?=e3Of(hor0_!x?9sQE=plZNX@V*;rVg zHj!t2s@v*X1HuM%Gd2mxcn0A7;E8oEVM7j|sSB>m^wrt}J+d9clZ^~T5o}*{r4L7# z2=+y;p=lh3{uR94CJya(W#YI*@_c9}&4h>IJV(AkvUz!Vaz?DY~ z3KYB0j;g5a2hLUVkA;}w&kEqJ3jykIIoKA~0JABkt&~ite4zs_*I{J; zW6=Ze2%eo2_qv)CWb~sRnJMv+(TefVuk-Euh zAp;`fW?scJ)71YL_9HP58k9}GfTNmXq8xuV=|zjFw+LKo^NivQlQSV;1I>EiEX90_ z=~jeUhqzQp2b26fBPKJLw%=9j(*DEY?)IrTrz-6#P>ho1C9YFZd5Ht?QuS70&h)r( z@(pnRY5$=B~$B8m;$enWPfoG;3}n{A8p?a zJ$1E^t3q`RMaa;0Rfb97!dQs3YSro$@T}`J@Y0pbrBB9Ex1n)d1&UF^m>};sQ5gWw zMw$ubw7IhcPK&w$sojyfHMQ_kvijxmDPrFmV{Z|zRxmj|(M}^gr zUZ9pk?+G&xt&dT%&1OE@%IbM-Q~i__UoQPgYHt4j#ihp~BJ{b2O&*Ot8|+P-)~}}8btayqnF&7tJ_T>l znyy{DmTuj;^#V5BLn)K%_rc9SFaH(NcPRCuPF&#Hc%wca**D}y@?hHAfs#z-$79PG zltC9SaQSA#Am~pbeWTfq#ME8Oer2W#t1S)wDm-p-5Nq!9RHb$9k!mAnmBY(=_)}M% zbY!;|ghkx-JH&)@9%(QA+G$k^CHQ&5Z2CAv?H?=S6ux#k6nV@{O-)@D?!zi#C7`Io zsF#Kte*)K&;yR9k3Pi_@qKxhr7iIP6juqLcZ?%ok?#7%%L_n+N(V-#MrgS?WAS zo%g9^UH2z1-mX6#J9bRlv}sc%>7Yza!qj<*=V|aP1>!|o^-a85;uk3I!y^|$9Vo*nnf=3*MFa^xG#4>ICpnOc{eFK;rl6xo!ka)C)8J%DH#t8)N(d_`J*b|8DsHu5l; zB@zb%xIZQlR|T?WLSJ&LMMl7wTa?jb79aMo4T4bnr!+F={H!Hk2zp-A1CxC%J1&c7 z;|NmDS!{j8L=$&u)W9sbk->jAFS8;7qKzgv^SJnU5Ak;@K5d_==4(sSUb)ZbyOnso zf-8OKOuip+IOSPGp9Z1~vXay}(AH9CC4q&T<(+tu@|b?_AwQW6!iAvO8cjBGxs?gw zI`~)&0o&|HJ(&=@@?6f-m!~6-r99Fg`9a|tt`E#oAU*q|;~rg`VQ7Izi%Ji!Ez0P9 zaeWBar>=b1vG~BQ523-}^E?^)Vo!ZY+UoLAbD5!R`wW)(VjEI~3`+De19LDh@gEUT zWVRGVyPW~^VCbf@Q-NY~2B`GqRxpF-8t6`d{y)_+M|vyyQT*C;aJbvl4%~>@wr$() zp&i@GMcZ>CR*13Sl1nbR2fEWiFArCRvZ%6QLMWwgW4Vs=BXCzjqa++16$ai$^4akJ zA1XW^Y1@pM(#8MV;x7wFq1sn+bhfcLv)eq~o1B|6ealJBVs&M(G_yzND$SV8S5f~B z9_Y9BkuT{EZUkr7l~h#xf`WpxoCplgn9an{Hv=x?w7)-!CuWuG738~Ydr5EP1=}{+ z^7CR`%hoE%=-6%|n7oEeu^cwePhtCCPg^}eZAZBbqJ#8dEjYpb18}q2 zNyYr;e+2X`&l~jqf@E#yL33)t=xA14AJ4-Qb9bA0Ilbmrm+AFi;iv6^rsn6esx7_%eo3iST?3zFGxlR%pnWP4RQkzt?Pof%ObZ_^_F8=vX{`I4kmz(RpTD@Gux2 zR-1V@{$2vM6-YoSIosFsarb}cCXahP`F3NyRy#PC_35b(a6l-z#9$(mO3 zKU9mpEx!Wo9a5atVNp*Aj2+hUT~-c*!F<*TvMZ0Iv-+fp9V zQNN_xSbR2xc55h*Foiq&*`myDomqwKHlxA+buqLFm0Hz=;;Q z&&me00M@werdqL~S-HerKD*Prm^ zxbzID^B>2N=)~V`^3Ff)g=LdD-)@ z@}&D`)Ie@#04A1ny8K6;B(v+tWrl|(mQw->5iZXj_<6qc6&%o|0-o+><_0q3eLUa_G}E;bi@^N>pHy^Od5sU-Zl z+yCNA^7q0sn!{j=o2QDEt(5=LJ=uTm1%2bKJiWLl1qEa)ka^XYJ#6Qh1}>} zN4%7ai!aypJ8KQkX>|+C{u@{|)t@8CNqXD^&VgNc-Q>KdJF<~|?o#f${i;6Kb>hxs z6DMd5_(k&;bv~h=_yA#cRCz|Q#R-WC1Jcve^HsPrZDjUsU2IgGXLyY^s7o7M*an{L z0?nfr_^VR(HP*jslrPILAS?N=X9pYl1AtDMoDO-+xQBV?Nu0pH#EN}NZk(JBa*35k zOM&LkpTCsVWK`ISy0-fM`|lqdw#)^(5@^N}sNMq^+K`BHFtkC77cZ6#xf@&wxDrq# zP_00U|6eILlkzVEt@8mB=*F%Dn!5xn1!AxBVMJl*+K#bf$I1_t+~7*Um4G4vOM&p8 zJpnwdXAI?5x8*VvV8fRTJjMt^`~OR7=3} z!kHlUIy&b;@UZ@WC=asU7T07*qoM6N<$ Eg5lktVgLXD diff --git a/notebook/static/base/js/dialog.js b/notebook/static/base/js/dialog.js deleted file mode 100644 index 77d81185f4..0000000000 --- a/notebook/static/base/js/dialog.js +++ /dev/null @@ -1,433 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -define(['jquery', - 'codemirror/lib/codemirror', - 'bootstrap', - 'base/js/i18n'], - function($, CodeMirror, bs, i18n) { - "use strict"; - - /** - * A wrapper around bootstrap modal for easier use - * Pass it an option dictionary with the following properties: - * - * - body : or , main content of the dialog - * if pass a it will be wrapped in a p tag and - * html element escaped, unless you specify sanitize=false - * option. - * - title : Dialog title, default to empty string. - * - buttons : dict of btn_options who keys are button label. - * see btn_options below for description - * - open : callback to trigger on dialog open. - * - destroy: - * - notebook : notebook instance - * - keyboard_manager: keyboard manager instance. - * - * Unlike bootstrap modals, the backdrop options is set by default - * to 'static'. - * - * The rest of the options are passed as is to bootstrap modals. - * - * btn_options: dict with the following property: - * - * - click : callback to trigger on click - * - class : css classes to add to button. - * - * - * - **/ - var modal = function (options) { - - var modal = $("
") - .addClass("modal") - .addClass("fade") - .attr("role", "dialog"); - var dialog = $("
") - .addClass("modal-dialog") - .appendTo(modal); - var dialog_content = $("
") - .addClass("modal-content") - .appendTo(dialog); - if(typeof(options.body) === 'string' && options.sanitize !== false){ - options.body = $("

").text(options.body); - } - dialog_content.append( - $("

") - .addClass("modal-header") - .mousedown(function() { - $(".modal").draggable({handle: '.modal-header'}); - }) - .append($("