From d040e51271ffbff4660d28cad422b7e743e75f9b Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 8 Mar 2021 15:28:03 -0800 Subject: [PATCH 1/7] Remove some of ipython_genutils no-op. ipython_genutils is complicated to package on some linux distro because of nose, and in general not useful as it mostly offered python 2/3 compat layer. Goal always has been to remove any usage of ipython_genutils --- jupyter_server/base/handlers.py | 3 +-- jupyter_server/serverapp.py | 9 +++---- jupyter_server/services/config/handlers.py | 1 - .../services/contents/filecheckpoints.py | 3 +-- jupyter_server/services/contents/fileio.py | 3 +-- .../services/contents/filemanager.py | 5 ++-- jupyter_server/services/contents/manager.py | 3 +-- .../services/kernels/kernelmanager.py | 3 +-- .../services/sessions/sessionmanager.py | 3 +-- .../tests/nbconvert/test_handlers.py | 2 +- jupyter_server/tests/test_gateway.py | 25 ++++++++++++------- jupyter_server/utils.py | 10 +++----- 12 files changed, 31 insertions(+), 39 deletions(-) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index b128f7b602..743f12d31d 100755 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -26,7 +26,6 @@ from traitlets.config import Application from ipython_genutils.path import filefind -from ipython_genutils.py3compat import string_types from jupyter_core.paths import is_hidden import jupyter_server @@ -785,7 +784,7 @@ def set_headers(self): def initialize(self, path, default_filename=None, no_cache_paths=None): self.no_cache_paths = no_cache_paths or [] - if isinstance(path, string_types): + if isinstance(path, str): path = [path] self.root = tuple( diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index b4584e0542..b13e5002b5 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -95,7 +95,6 @@ Any, Dict, Unicode, Integer, List, Bool, Bytes, Instance, TraitError, Type, Float, observe, default, validate ) -from ipython_genutils import py3compat from jupyter_core.paths import jupyter_runtime_dir, jupyter_path from jupyter_server._sysinfo import get_sys_info @@ -201,7 +200,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager, "template_path", jupyter_app.template_file_path, ) - if isinstance(_template_path, py3compat.string_types): + if isinstance(_template_path, str): _template_path = (_template_path,) template_path = [os.path.expanduser(path) for path in _template_path] @@ -229,7 +228,7 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager, now = utcnow() root_dir = contents_manager.root_dir - home = py3compat.str_to_unicode(os.path.expanduser('~'), encoding=sys.getfilesystemencoding()) + home = os.path.expanduser("~") if root_dir.startswith(home + os.path.sep): # collapse $HOME to ~ root_dir = '~' + root_dir[len(home):] @@ -953,8 +952,6 @@ def _default_allow_remote(self): # Address is a hostname for info in socket.getaddrinfo(self.ip, self.port, 0, socket.SOCK_STREAM): addr = info[4][0] - if not py3compat.PY3: - addr = addr.decode('ascii') try: parsed = ipaddress.ip_address(addr.split('%')[0]) @@ -1281,7 +1278,7 @@ def _default_root_dir(self): self._root_dir_set = True return os.path.dirname(os.path.abspath(self.file_to_run)) else: - return py3compat.getcwd() + return os.getcwd() @validate('root_dir') def _root_dir_validate(self, proposal): diff --git a/jupyter_server/services/config/handlers.py b/jupyter_server/services/config/handlers.py index 76c1bd3e56..aae6480757 100644 --- a/jupyter_server/services/config/handlers.py +++ b/jupyter_server/services/config/handlers.py @@ -8,7 +8,6 @@ import errno from tornado import web -from ipython_genutils.py3compat import PY3 from ...base.handlers import APIHandler class ConfigHandler(APIHandler): diff --git a/jupyter_server/services/contents/filecheckpoints.py b/jupyter_server/services/contents/filecheckpoints.py index 757429a217..e6091ecbf5 100644 --- a/jupyter_server/services/contents/filecheckpoints.py +++ b/jupyter_server/services/contents/filecheckpoints.py @@ -16,7 +16,6 @@ from anyio import run_sync_in_worker_thread from jupyter_core.utils import ensure_dir_exists -from ipython_genutils.py3compat import getcwd from traitlets import Unicode from jupyter_server import _tz as tz @@ -48,7 +47,7 @@ def _root_dir_default(self): try: return self.parent.root_dir except AttributeError: - return getcwd() + return os.getcwd() # ContentsManager-dependent checkpoint API def create_checkpoint(self, contents_mgr, path): diff --git a/jupyter_server/services/contents/fileio.py b/jupyter_server/services/contents/fileio.py index 3311f455b0..58e28576ce 100644 --- a/jupyter_server/services/contents/fileio.py +++ b/jupyter_server/services/contents/fileio.py @@ -21,7 +21,6 @@ ) import nbformat -from ipython_genutils.py3compat import str_to_unicode from traitlets.config import Configurable from traitlets import Bool @@ -231,7 +230,7 @@ def perm_to_403(self, os_path=''): # this may not work perfectly on unicode paths on Python 2, # but nobody should be doing that anyway. if not os_path: - os_path = str_to_unicode(e.filename or 'unknown file') + os_path = 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 else: diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 51f55b4044..6f465cf9e8 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -22,7 +22,6 @@ from ipython_genutils.importstring import import_item from traitlets import Any, Unicode, Bool, TraitError, observe, default, validate -from ipython_genutils.py3compat import getcwd, string_types from jupyter_core.paths import exists, is_hidden, is_file_hidden from jupyter_server import _tz as tz @@ -47,7 +46,7 @@ def _default_root_dir(self): try: return self.parent.root_dir except AttributeError: - return getcwd() + return os.getcwd() post_save_hook = Any(None, config=True, allow_none=True, help="""Python callable or importstring thereof @@ -70,7 +69,7 @@ def _default_root_dir(self): @validate('post_save_hook') def _validate_post_save_hook(self, proposal): value = proposal['value'] - if isinstance(value, string_types): + if isinstance(value, str): value = import_item(value) if not callable(value): raise TraitError("post_save_hook must be callable") diff --git a/jupyter_server/services/contents/manager.py b/jupyter_server/services/contents/manager.py index caf7bbf10e..6a749d9a51 100644 --- a/jupyter_server/services/contents/manager.py +++ b/jupyter_server/services/contents/manager.py @@ -29,7 +29,6 @@ validate, default, ) -from ipython_genutils.py3compat import string_types from jupyter_server.transutils import _i18n from jupyter_server.utils import ensure_async @@ -106,7 +105,7 @@ def _notary_default(self): @validate('pre_save_hook') def _validate_pre_save_hook(self, proposal): value = proposal['value'] - if isinstance(value, string_types): + if isinstance(value, str): value = import_item(self.pre_save_hook) if not callable(value): raise TraitError("pre_save_hook must be callable") diff --git a/jupyter_server/services/kernels/kernelmanager.py b/jupyter_server/services/kernels/kernelmanager.py index b5f14dbde0..043d7d1e57 100644 --- a/jupyter_server/services/kernels/kernelmanager.py +++ b/jupyter_server/services/kernels/kernelmanager.py @@ -25,7 +25,6 @@ from jupyter_server.utils import to_os_path, ensure_async from jupyter_server._tz import utcnow, isoformat -from ipython_genutils.py3compat import getcwd from jupyter_server.prometheus.metrics import KERNEL_CURRENTLY_RUNNING_TOTAL @@ -58,7 +57,7 @@ def _default_root_dir(self): try: return self.parent.root_dir except AttributeError: - return getcwd() + return os.getcwd() @validate('root_dir') def _update_root_dir(self, proposal): diff --git a/jupyter_server/services/sessions/sessionmanager.py b/jupyter_server/services/sessions/sessionmanager.py index eeba4a352d..1ecff95806 100644 --- a/jupyter_server/services/sessions/sessionmanager.py +++ b/jupyter_server/services/sessions/sessionmanager.py @@ -14,7 +14,6 @@ from tornado import web from traitlets.config.configurable import LoggingConfigurable -from ipython_genutils.py3compat import unicode_type from traitlets import Instance from jupyter_server.utils import ensure_async @@ -81,7 +80,7 @@ async def session_exists(self, path): def new_session_id(self): "Create a uuid for a new session" - return unicode_type(uuid.uuid4()) + return str(uuid.uuid4()) async def create_session(self, path=None, name=None, type=None, kernel_name=None, kernel_id=None): """Creates a session and returns its model""" diff --git a/jupyter_server/tests/nbconvert/test_handlers.py b/jupyter_server/tests/nbconvert/test_handlers.py index 8dfc7b7307..9b5dfce6aa 100644 --- a/jupyter_server/tests/nbconvert/test_handlers.py +++ b/jupyter_server/tests/nbconvert/test_handlers.py @@ -8,7 +8,7 @@ new_notebook, new_markdown_cell, new_code_cell, new_output, ) -from ipython_genutils.py3compat import which +from shutil import which from base64 import encodebytes diff --git a/jupyter_server/tests/test_gateway.py b/jupyter_server/tests/test_gateway.py index 2eb03c3a98..e6311e6f7a 100644 --- a/jupyter_server/tests/test_gateway.py +++ b/jupyter_server/tests/test_gateway.py @@ -7,7 +7,6 @@ from datetime import datetime from tornado.web import HTTPError from tornado.httpclient import HTTPRequest, HTTPResponse -from ipython_genutils.py3compat import str_to_unicode from jupyter_server.serverapp import ServerApp from jupyter_server.gateway.managers import GatewayClient from jupyter_server.utils import ensure_async @@ -51,7 +50,7 @@ async def mock_gateway_request(url, **kwargs): # Fetch all kernelspecs if endpoint.endswith('/api/kernelspecs') and method == 'GET': - response_buf = StringIO(str_to_unicode(json.dumps(kernelspecs))) + response_buf = StringIO(json.dumps(kernelspecs)) response = await ensure_async(HTTPResponse(request, 200, buffer=response_buf)) return response @@ -60,7 +59,7 @@ async def mock_gateway_request(url, **kwargs): requested_kernelspec = endpoint.rpartition('/')[2] kspecs = kernelspecs.get('kernelspecs') if requested_kernelspec in kspecs: - response_buf = StringIO(str_to_unicode(json.dumps(kspecs.get(requested_kernelspec)))) + response_buf = StringIO(json.dumps(kspecs.get(requested_kernelspec))) response = await ensure_async(HTTPResponse(request, 200, buffer=response_buf)) return response else: @@ -75,7 +74,7 @@ async def mock_gateway_request(url, **kwargs): assert name == kspec_name # Ensure that KERNEL_ env values get propagated model = generate_model(name) running_kernels[model.get('id')] = model # Register model as a running kernel - response_buf = StringIO(str_to_unicode(json.dumps(model))) + response_buf = StringIO(json.dumps(model)) response = await ensure_async(HTTPResponse(request, 201, buffer=response_buf)) return response @@ -85,7 +84,7 @@ async def mock_gateway_request(url, **kwargs): for kernel_id in running_kernels.keys(): model = running_kernels.get(kernel_id) kernels.append(model) - response_buf = StringIO(str_to_unicode(json.dumps(kernels))) + response_buf = StringIO(json.dumps(kernels)) response = await ensure_async(HTTPResponse(request, 200, buffer=response_buf)) return response @@ -101,8 +100,12 @@ async def mock_gateway_request(url, **kwargs): raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id) elif action == 'restart': if requested_kernel_id in running_kernels: - response_buf = StringIO(str_to_unicode(json.dumps(running_kernels.get(requested_kernel_id)))) - response = await ensure_async(HTTPResponse(request, 204, buffer=response_buf)) + response_buf = StringIO( + json.dumps(running_kernels.get(requested_kernel_id)) + ) + response = await ensure_async( + HTTPResponse(request, 204, buffer=response_buf) + ) return response else: raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id) @@ -120,8 +123,12 @@ async def mock_gateway_request(url, **kwargs): if endpoint.rfind('/api/kernels/') >= 0 and method == 'GET': requested_kernel_id = endpoint.rpartition('/')[2] if requested_kernel_id in running_kernels: - response_buf = StringIO(str_to_unicode(json.dumps(running_kernels.get(requested_kernel_id)))) - response = await ensure_async(HTTPResponse(request, 200, buffer=response_buf)) + response_buf = StringIO( + json.dumps(running_kernels.get(requested_kernel_id)) + ) + response = await ensure_async( + HTTPResponse(request, 200, buffer=response_buf) + ) return response else: raise HTTPError(404, message='Kernel does not exist: %s' % requested_kernel_id) diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index 04b1175e9b..842a62e3ed 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -13,7 +13,6 @@ from urllib.parse import quote, unquote, urlparse, urljoin from urllib.request import pathname2url -from ipython_genutils import py3compat def url_path_join(*pieces): @@ -59,8 +58,8 @@ 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]) + parts = path.split("/") + return "/".join([quote(p) for p in parts]) def url_unescape(path): @@ -68,10 +67,7 @@ def url_unescape(path): Turns '/foo%20bar/' into '/foo bar/' """ - return u'/'.join([ - py3compat.str_to_unicode(unquote(p), encoding='utf8') - for p in py3compat.unicode_to_str(path, encoding='utf8').split('/') - ]) + return "/".join([unquote(p) for p in path.split("/")]) def samefile_simple(path, other_path): From 4f4eab2a5527d61a50c8e88184e739f1abcf116c Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 24 Apr 2021 10:46:15 -0700 Subject: [PATCH 2/7] DOC: Autoreformat docstrings. This uses an autoreformatter to reformat and fix some of the common mistakes in numpydoc. Some of this is purely visual, but other like space before colon in parameters actually have semantic values. In the case of space before colon this is to make sure that nupmydoc properly distinguish the parameter name from its type. --- jupyter_server/_sysinfo.py | 12 ++++----- jupyter_server/_tz.py | 2 +- jupyter_server/auth/login.py | 2 +- jupyter_server/base/zmqhandlers.py | 2 -- jupyter_server/extension/serverextension.py | 1 - jupyter_server/gateway/managers.py | 6 ++--- jupyter_server/serverapp.py | 11 +++----- .../services/contents/checkpoints.py | 24 ++++++++--------- jupyter_server/services/contents/fileio.py | 26 +++++++------------ jupyter_server/services/contents/manager.py | 4 +-- .../services/kernels/kernelmanager.py | 6 ++--- .../tests/services/sessions/test_api.py | 10 +++---- jupyter_server/utils.py | 10 +++---- 13 files changed, 52 insertions(+), 64 deletions(-) diff --git a/jupyter_server/_sysinfo.py b/jupyter_server/_sysinfo.py index 731e09f8e3..ae0999fe9e 100644 --- a/jupyter_server/_sysinfo.py +++ b/jupyter_server/_sysinfo.py @@ -26,15 +26,15 @@ def pkg_commit_hash(pkg_path): Parameters ---------- pkg_path : str - directory containing package - only used for getting commit from active repo + directory containing package + only used for getting commit from active repo Returns ------- hash_from : str - Where we got the hash from - description + Where we got the hash from - description hash_str : str - short form of hash + short form of hash """ # maybe we are in a repository, check for a .git folder @@ -68,12 +68,12 @@ def pkg_info(pkg_path): Parameters ---------- pkg_path : str - path containing __init__.py for package + path containing __init__.py for package Returns ------- context : dict - with named parameters of interest + with named parameters of interest """ src, hsh = pkg_commit_hash(pkg_path) return dict( diff --git a/jupyter_server/_tz.py b/jupyter_server/_tz.py index 1df39e51d4..84ba012efa 100644 --- a/jupyter_server/_tz.py +++ b/jupyter_server/_tz.py @@ -36,7 +36,7 @@ def utc_method(*args, **kwargs): def isoformat(dt): """Return iso-formatted timestamp - + Like .isoformat(), but uses Z for UTC instead of +00:00 """ return dt.isoformat().replace('+00:00', 'Z') diff --git a/jupyter_server/auth/login.py b/jupyter_server/auth/login.py index 76c18bab08..d0a2d722a4 100644 --- a/jupyter_server/auth/login.py +++ b/jupyter_server/auth/login.py @@ -196,7 +196,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 diff --git a/jupyter_server/base/zmqhandlers.py b/jupyter_server/base/zmqhandlers.py index c62d0b9077..839d4f38eb 100644 --- a/jupyter_server/base/zmqhandlers.py +++ b/jupyter_server/base/zmqhandlers.py @@ -32,7 +32,6 @@ def serialize_binary_message(msg): Returns ------- - The message serialized to bytes. """ @@ -64,7 +63,6 @@ def deserialize_binary_message(bmsg): Returns ------- - message dictionary """ nbufs = struct.unpack('!i', bmsg[:4])[0] diff --git a/jupyter_server/extension/serverextension.py b/jupyter_server/extension/serverextension.py index fbb32ad431..d716ed1ff8 100644 --- a/jupyter_server/extension/serverextension.py +++ b/jupyter_server/extension/serverextension.py @@ -27,7 +27,6 @@ def _get_config_dir(user=False, sys_prefix=False): Parameters ---------- - user : bool [default: False] Get the user's .jupyter config directory sys_prefix : bool [default: False] diff --git a/jupyter_server/gateway/managers.py b/jupyter_server/gateway/managers.py index 82f8cb28e0..61a9a4bb4d 100644 --- a/jupyter_server/gateway/managers.py +++ b/jupyter_server/gateway/managers.py @@ -293,7 +293,7 @@ def init_static_args(self): def load_connection_args(self, **kwargs): """Merges the static args relative to the connection, with the given keyword arguments. If statics - have yet to be initialized, we'll do that here. + have yet to be initialized, we'll do that here. """ if len(self._static_args) == 0: @@ -360,7 +360,7 @@ def _get_kernel_endpoint_url(self, kernel_id=None): Parameters ---------- - kernel_id: kernel UUID (optional) + kernel_id : kernel UUID (optional) """ if kernel_id: return url_path_join(self.base_endpoint, url_escape(str(kernel_id))) @@ -562,7 +562,7 @@ def _get_kernelspecs_endpoint_url(self, kernel_name=None): Parameters ---------- - kernel_name: kernel name (optional) + kernel_name : kernel name (optional) """ if kernel_name: return url_path_join(self.base_endpoint, url_escape(kernel_name)) diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index b13e5002b5..24c1227e6e 100755 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1823,19 +1823,16 @@ def initialize(self, argv=None, find_extensions=True, new_httpserver=True, start Parameters ---------- - argv: list or None + argv : list or None CLI arguments to parse. - - find_extensions: bool + find_extensions : bool If True, find and load extensions listed in Jupyter config paths. If False, only load extensions that are passed to ServerApp directy through the `argv`, `config`, or `jpserver_extensions` arguments. - - new_httpserver: bool + new_httpserver : bool If True, a tornado HTTPServer instance will be created and configured for the Server Web Application. This will set the http_server attribute of this class. - - starter_extension: str + starter_extension : str If given, it references the name of an extension point that started the Server. We will try to load configuration from extension point """ diff --git a/jupyter_server/services/contents/checkpoints.py b/jupyter_server/services/contents/checkpoints.py index 4e86b4c0e0..7ac5bce4ba 100644 --- a/jupyter_server/services/contents/checkpoints.py +++ b/jupyter_server/services/contents/checkpoints.py @@ -121,12 +121,12 @@ def create_notebook_checkpoint(self, nb, path): def get_file_checkpoint(self, checkpoint_id, path): """Get the content of a checkpoint for a non-notebook file. - Returns a dict of the form: - { - 'type': 'file', - 'content': , - 'format': {'text','base64'}, - } + Returns a dict of the form: + { + 'type': 'file', + 'content': , + 'format': {'text','base64'}, + } """ raise NotImplementedError("must be implemented in a subclass") @@ -229,12 +229,12 @@ async def create_notebook_checkpoint(self, nb, path): async def get_file_checkpoint(self, checkpoint_id, path): """Get the content of a checkpoint for a non-notebook file. - Returns a dict of the form: - { - 'type': 'file', - 'content': , - 'format': {'text','base64'}, - } + Returns a dict of the form: + { + 'type': 'file', + 'content': , + 'format': {'text','base64'}, + } """ raise NotImplementedError("must be implemented in a subclass") diff --git a/jupyter_server/services/contents/fileio.py b/jupyter_server/services/contents/fileio.py index 58e28576ce..cd10928262 100644 --- a/jupyter_server/services/contents/fileio.py +++ b/jupyter_server/services/contents/fileio.py @@ -86,17 +86,14 @@ def atomic_writing(path, text=True, encoding='utf-8', log=None, **kwargs): Parameters ---------- path : str - The target file to write to. - + The target file to write to. text : bool, optional - Whether to open the file in text mode (i.e. to write unicode). Default is - True. - + Whether to open the file in text mode (i.e. to write unicode). Default is + True. encoding : str, optional - The encoding to use for files opened in text mode. Default is UTF-8. - + The encoding to use for files opened in text mode. Default is UTF-8. **kwargs - Passed to :func:`io.open`. + Passed to :func:`io.open`. """ # realpath doesn't work on Windows: https://bugs.python.org/issue9949 # Luckily, we only need to resolve the file itself being a symlink, not @@ -142,17 +139,14 @@ def _simple_writing(path, text=True, encoding='utf-8', log=None, **kwargs): Parameters ---------- path : str - The target file to write to. - + The target file to write to. text : bool, optional - Whether to open the file in text mode (i.e. to write unicode). Default is - True. - + Whether to open the file in text mode (i.e. to write unicode). Default is + True. encoding : str, optional - The encoding to use for files opened in text mode. Default is UTF-8. - + The encoding to use for files opened in text mode. Default is UTF-8. **kwargs - Passed to :func:`io.open`. + Passed to :func:`io.open`. """ # realpath doesn't work on Windows: https://bugs.python.org/issue9949 # Luckily, we only need to resolve the file itself being a symlink, not diff --git a/jupyter_server/services/contents/manager.py b/jupyter_server/services/contents/manager.py index 6a749d9a51..e45be5f4ed 100644 --- a/jupyter_server/services/contents/manager.py +++ b/jupyter_server/services/contents/manager.py @@ -326,7 +326,7 @@ def increment_filename(self, filename, path='', insert=''): The name of a file, including extension path : unicode The API path of the target's directory - insert: unicode + insert : unicode The characters to insert after the base filename Returns @@ -691,7 +691,7 @@ async def increment_filename(self, filename, path='', insert=''): The name of a file, including extension path : unicode The API path of the target's directory - insert: unicode + insert : unicode The characters to insert after the base filename Returns diff --git a/jupyter_server/services/kernels/kernelmanager.py b/jupyter_server/services/kernels/kernelmanager.py index 043d7d1e57..d0bd3fa00d 100644 --- a/jupyter_server/services/kernels/kernelmanager.py +++ b/jupyter_server/services/kernels/kernelmanager.py @@ -251,11 +251,11 @@ def start_buffering(self, kernel_id, session_key, channels): ---------- kernel_id : str The id of the kernel to stop buffering. - session_key: str + session_key : str The session_key, if any, that should get the buffer. If the session_key matches the current buffered session_key, the buffer will be returned. - channels: dict({'channel': ZMQStream}) + channels : dict({'channel': ZMQStream}) The zmq channels whose messages should be buffered. """ @@ -290,7 +290,7 @@ def get_buffer(self, kernel_id, session_key): ---------- kernel_id : str The id of the kernel to stop buffering. - session_key: str, optional + session_key : str, optional The session_key, if any, that should get the buffer. If the session_key matches the current buffered session_key, the buffer will be returned. diff --git a/jupyter_server/tests/services/sessions/test_api.py b/jupyter_server/tests/services/sessions/test_api.py index 0208880b5f..c7c06e24c0 100644 --- a/jupyter_server/tests/services/sessions/test_api.py +++ b/jupyter_server/tests/services/sessions/test_api.py @@ -150,9 +150,9 @@ def session_client(jp_root_dir, jp_fetch): def assert_kernel_equality(actual, expected): """ Compares kernel models after taking into account that execution_states - may differ from 'starting' to 'idle'. The 'actual' argument is the - current state (which may have an 'idle' status) while the 'expected' - argument is the previous state (which may have a 'starting' status). + may differ from 'starting' to 'idle'. The 'actual' argument is the + current state (which may have an 'idle' status) while the 'expected' + argument is the previous state (which may have a 'starting' status). """ actual.pop('execution_state', None) actual.pop('last_activity', None) @@ -163,8 +163,8 @@ def assert_kernel_equality(actual, expected): def assert_session_equality(actual, expected): """ Compares session models. `actual` is the most current session, - while `expected` is the target of the comparison. This order - matters when comparing the kernel sub-models. + while `expected` is the target of the comparison. This order + matters when comparing the kernel sub-models. """ assert actual['id'] == expected['id'] assert actual['path'] == expected['path'] diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index 842a62e3ed..7a86f38581 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -82,12 +82,12 @@ def samefile_simple(path, other_path): Only to be used if os.path.samefile is not available. Parameters - ----------- - path: String representing a path to a file - other_path: String representing a path to another file + ---------- + path : String representing a path to a file + other_path : String representing a path to another file Returns - ----------- + ------- same: Boolean that is True if both path and other path are the same """ path_stat = os.stat(path) @@ -193,7 +193,7 @@ def run_sync(maybe_async): Returns ------- - result : + result Whatever the async object returns, or the object itself. """ if not inspect.isawaitable(maybe_async): From e6b46cdbaf3ad8b588cc7149f28393d0e3c2ea5a Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Sun, 25 Apr 2021 13:25:43 +0200 Subject: [PATCH 3/7] Prep for Release Helper Usage --- .github/workflows/check-release.yml | 50 +++++++++++++++++++++++++++++ .github/workflows/python-linux.yml | 10 ------ CHANGELOG.md | 4 +++ MANIFEST.in | 9 +++--- RELEASE.md | 34 +++++++++++++------- 5 files changed, 82 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/check-release.yml diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml new file mode 100644 index 0000000000..2ab93123de --- /dev/null +++ b/.github/workflows/check-release.yml @@ -0,0 +1,50 @@ +name: Check Release +on: + push: + branches: ["master"] + pull_request: + branches: ["*"] + +jobs: + check_release: + runs-on: ubuntu-latest + permissions: + contents: + write + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + architecture: "x64" + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + - name: Cache pip + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}-pip- + - name: Cache checked links + uses: actions/cache@v2 + with: + path: ~/.cache/pytest-link-check + key: ${{ runner.os }}-linkcheck-${{ hashFiles('**/.md') }}-md-links + restore-keys: | + ${{ runner.os }}-linkcheck- + - name: Upgrade packaging dependencies + run: | + pip install --upgrade pip setuptools wheel --user + - name: Install Dependencies + run: | + pip install -e . + - name: Check Release + uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/python-linux.yml b/.github/workflows/python-linux.yml index 8fedfc3ecc..eed4081e39 100644 --- a/.github/workflows/python-linux.yml +++ b/.github/workflows/python-linux.yml @@ -68,13 +68,3 @@ jobs: pushd test_install ./bin/pytest --pyargs jupyter_server popd - - name: Check the Manifest - run: | - pip install check-manifest - git clean -dfx - check-manifest -v - - name: Check Version Bump - run: | - pip install tbump - tbump --non-interactive --only-patch 100.1.1 - git checkout . diff --git a/CHANGELOG.md b/CHANGELOG.md index 4295da3404..8771c93299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. + + ## 1.6.4 ([Full Changelog](https://github.com/jupyter-server/jupyter_server/compare/v1.6.3...68a64ea13be5d0d86460f04e0c47eb0b6662a0af)) @@ -16,6 +18,8 @@ All notable changes to this project will be documented in this file. [@afshin](https://github.com/search?q=repo%3Ajupyter-server%2Fjupyter_server+involves%3Aafshin+updated%3A2021-04-21..2021-04-21&type=Issues) + + ## 1.6.3 ([Full Changelog](https://github.com/jupyter-server/jupyter_server/compare/v1.6.2...aa2636795ae1d87e3055febb3931f891dd6b4451)) diff --git a/MANIFEST.in b/MANIFEST.in index 84c24303cc..70ccbaaa91 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -25,7 +25,8 @@ prune docs/dist global-exclude *~ global-exclude *.pyc global-exclude *.pyo -global-exclude .git -global-exclude .ipynb_checkpoints -global-exclude .pytest_cache -global-exclude .coverage +prune .git +prune **/.ipynb_checkpoints +prune **/.pytest_cache +prune **/.coverage +prune **/.pytest_cache diff --git a/RELEASE.md b/RELEASE.md index b492af54d0..a785a28701 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,36 +1,48 @@ # Making a Jupyter Server Release -To create a release, perform the following steps... -## Set up -``` +## Using `jupyter_releaser` + +The recommended way to make a release is to use [`jupyter_releaser`](https://github.com/jupyter-server/jupyter_releaser#checklist-for-adoption). + +## Manual Release + +To create a manual release, perform the following steps: + +### Set up + +```bash pip install tbump twine build git pull origin $(git branch --show-current) git clean -dffx ``` -## Update the version and apply the tag -``` +### Update the version and apply the tag + +```bash echo "Enter new version" read script_version tbump ${script_version} ``` -## Build the artifacts -``` +### Build the artifacts + +```bash rm -rf dist python -m build . ``` -## Update the version back to dev -``` +### Update the version back to dev + +```bash echo "Enter dev version" read dev_version tbump ${dev_version} --no-tag git push origin $(git branch --show-current) ``` -## Publish the artifacts to pypi -``` +### Publish the artifacts to pypi + +```bash twine check dist/* twine upload dist/* ``` From d80df5b468f31470961d796bd687f7fb8e80f412 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Fri, 30 Apr 2021 12:47:44 +0200 Subject: [PATCH 4/7] Remove permissions from CI workflow --- .github/workflows/check-release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index 2ab93123de..335ab9d037 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -8,9 +8,6 @@ on: jobs: check_release: runs-on: ubuntu-latest - permissions: - contents: - write steps: - name: Checkout uses: actions/checkout@v2 From 80d9ad5130fb449cf050fe87f4ae6a8c53c694d2 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Thu, 29 Apr 2021 16:37:03 -0700 Subject: [PATCH 5/7] Fix for recursive symlink - Notebook 4670 This change also prevents permission-related exceptions from logging the file. Co-authored-by: Shane Canon Co-authored-by: Thomas Kluyver --- .../services/contents/filemanager.py | 37 ++++++++++++++----- .../tests/services/contents/test_manager.py | 37 +++++++++++++++---- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 6f465cf9e8..5166969427 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -273,7 +273,7 @@ def _dir_model(self, path, content=True): # skip over broken symlinks in listing if e.errno == errno.ENOENT: self.log.warning("%s doesn't exist", os_path) - else: + elif e.errno != errno.EACCES: # Don't provide clues about protected files self.log.warning("Error stat-ing %s: %s", os_path, e) continue @@ -283,17 +283,25 @@ def _dir_model(self, path, content=True): self.log.debug("%s not a regular file", os_path) continue - if self.should_list(name): - if self.allow_hidden or not is_file_hidden(os_path, stat_res=st): - contents.append( + try: + 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) + ) + except OSError as e: + # ELOOP: recursive symlink, also don't show failure due to permissions + if e.errno not in [errno.ELOOP, errno.EACCES]: + self.log.warning( + "Unknown error checking if file %r is hidden", + os_path, + exc_info=True, ) model['format'] = 'json' return model - def _file_model(self, path, content=True, format=None): """Build a model for a file @@ -585,7 +593,7 @@ async def _dir_model(self, path, content=True): # skip over broken symlinks in listing if e.errno == errno.ENOENT: self.log.warning("%s doesn't exist", os_path) - else: + elif e.errno != errno.EACCES: # Don't provide clues about protected files self.log.warning("Error stat-ing %s: %s", os_path, e) continue @@ -595,10 +603,19 @@ async def _dir_model(self, path, content=True): self.log.debug("%s not a regular file", os_path) continue - if self.should_list(name): - if self.allow_hidden or not is_file_hidden(os_path, stat_res=st): - contents.append( - await self.get(path='%s/%s' % (path, name), content=False) + try: + if self.should_list(name): + if self.allow_hidden or not is_file_hidden(os_path, stat_res=st): + contents.append( + await self.get(path='%s/%s' % (path, name), content=False) + ) + except OSError as e: + # ELOOP: recursive symlink, also don't show failure due to permissions + if e.errno not in [errno.ELOOP, errno.EACCES]: + self.log.warning( + "Unknown error checking if file %r is hidden", + os_path, + exc_info=True, ) model['format'] = 'json' diff --git a/jupyter_server/tests/services/contents/test_manager.py b/jupyter_server/tests/services/contents/test_manager.py index 27534dd8fc..98f358645c 100644 --- a/jupyter_server/tests/services/contents/test_manager.py +++ b/jupyter_server/tests/services/contents/test_manager.py @@ -2,18 +2,17 @@ import sys import time import pytest -import functools from traitlets import TraitError from tornado.web import HTTPError from itertools import combinations - from nbformat import v4 as nbformat from jupyter_server.services.contents.filemanager import AsyncFileContentsManager, FileContentsManager from jupyter_server.utils import ensure_async from ...utils import expected_http_error + @pytest.fixture(params=[(FileContentsManager, True), (FileContentsManager, False), (AsyncFileContentsManager, True), @@ -29,6 +28,7 @@ def file_contents_manager_class(request, tmp_path): # -------------- Functions ---------------------------- + def _make_dir(jp_contents_manager, api_path): """ Make a directory. @@ -99,6 +99,7 @@ async def check_populated_dir_files(jp_contents_manager, api_path): # ----------------- Tests ---------------------------------- + def test_root_dir(file_contents_manager_class, tmp_path): fm = file_contents_manager_class(root_dir=str(tmp_path)) assert fm.root_dir == str(tmp_path) @@ -116,6 +117,7 @@ def test_invalid_root_dir(file_contents_manager_class, tmp_path): with pytest.raises(TraitError): file_contents_manager_class(root_dir=str(temp_file)) + def test_get_os_path(file_contents_manager_class, tmp_path): fm = file_contents_manager_class(root_dir=str(tmp_path)) path = fm._get_os_path('/path/to/notebook/test.ipynb') @@ -146,10 +148,6 @@ def test_checkpoint_subdir(file_contents_manager_class, tmp_path): assert cp_dir == os.path.join(str(tmp_path), cpm.checkpoint_dir, cp_name) -@pytest.mark.skipif( - sys.platform == 'win32' and sys.version_info[0] < 3, - reason="System platform is Windows, version < 3" -) async def test_bad_symlink(file_contents_manager_class, tmp_path): td = str(tmp_path) @@ -172,9 +170,31 @@ async def test_bad_symlink(file_contents_manager_class, tmp_path): @pytest.mark.skipif( - sys.platform == 'win32' and sys.version_info[0] < 3, - reason="System platform is Windows, version < 3" + sys.platform.startswith('win'), + reason="Windows doesn't detect symlink loops" ) +async def test_recursive_symlink(file_contents_manager_class, tmp_path): + td = str(tmp_path) + + cm = file_contents_manager_class(root_dir=td) + path = 'test recursive symlink' + _make_dir(cm, path) + + file_model = await ensure_async(cm.new_untitled(path=path, ext='.txt')) + + # create recursive symlink + symlink(cm, '%s/%s' % (path, "recursive"), '%s/%s' % (path, "recursive")) + model = await ensure_async(cm.get(path)) + + contents = { + content['name']: content for content in model['content'] + } + assert 'untitled.txt' in contents + assert contents['untitled.txt'] == file_model + # recursive symlinks should not be shown in the contents manager + assert 'recursive' not in contents + + async def test_good_symlink(file_contents_manager_class, tmp_path): td = str(tmp_path) cm = file_contents_manager_class(root_dir=td) @@ -213,6 +233,7 @@ async def test_403(file_contents_manager_class, tmp_path): except HTTPError as e: assert e.status_code == 403 + async def test_escape_root(file_contents_manager_class, tmp_path): td = str(tmp_path) cm = file_contents_manager_class(root_dir=td) From 02d2e8bfba14f14d5fe67bafb4424a8ddb550a42 Mon Sep 17 00:00:00 2001 From: Mariko Wakabayashi Date: Sat, 24 Apr 2021 12:35:52 -0600 Subject: [PATCH 6/7] Upgrade anyio to v3 --- .../services/contents/filecheckpoints.py | 8 ++-- jupyter_server/services/contents/fileio.py | 16 +++---- .../services/contents/filemanager.py | 20 ++++---- .../services/contents/largefilemanager.py | 4 +- .../tests/services/contents/test_manager.py | 48 +++++++++---------- setup.cfg | 2 +- 6 files changed, 48 insertions(+), 50 deletions(-) diff --git a/jupyter_server/services/contents/filecheckpoints.py b/jupyter_server/services/contents/filecheckpoints.py index e6091ecbf5..5c54f0f360 100644 --- a/jupyter_server/services/contents/filecheckpoints.py +++ b/jupyter_server/services/contents/filecheckpoints.py @@ -14,7 +14,7 @@ ) from .fileio import AsyncFileManagerMixin, FileManagerMixin -from anyio import run_sync_in_worker_thread +from anyio.to_thread import run_sync from jupyter_core.utils import ensure_dir_exists from traitlets import Unicode @@ -156,7 +156,7 @@ async def restore_checkpoint(self, contents_mgr, checkpoint_id, path): async def checkpoint_model(self, checkpoint_id, os_path): """construct the info dict for a given checkpoint""" - stats = await run_sync_in_worker_thread(os.stat, os_path) + stats = await run_sync(os.stat, os_path) last_modified = tz.utcfromtimestamp(stats.st_mtime) info = dict( id=checkpoint_id, @@ -176,7 +176,7 @@ async def rename_checkpoint(self, checkpoint_id, old_path, new_path): new_cp_path, ) with self.perm_to_403(): - await run_sync_in_worker_thread(shutil.move, old_cp_path, new_cp_path) + await run_sync(shutil.move, old_cp_path, new_cp_path) async def delete_checkpoint(self, checkpoint_id, path): """delete a file's checkpoint""" @@ -187,7 +187,7 @@ async def delete_checkpoint(self, checkpoint_id, path): self.log.debug("unlinking %s", cp_path) with self.perm_to_403(): - await run_sync_in_worker_thread(os.unlink, cp_path) + await run_sync(os.unlink, cp_path) async def list_checkpoints(self, path): """list the checkpoints for a given file diff --git a/jupyter_server/services/contents/fileio.py b/jupyter_server/services/contents/fileio.py index cd10928262..559139fdf0 100644 --- a/jupyter_server/services/contents/fileio.py +++ b/jupyter_server/services/contents/fileio.py @@ -12,7 +12,7 @@ import os import shutil -from anyio import run_sync_in_worker_thread +from anyio.to_thread import run_sync from tornado.web import HTTPError from jupyter_server.utils import ( @@ -36,7 +36,7 @@ def replace_file(src, dst): async def async_replace_file(src, dst): """ replace dst with src asynchronously """ - await run_sync_in_worker_thread(os.replace, src, dst) + await run_sync(os.replace, src, dst) def copy2_safe(src, dst, log=None): """copy src to dst @@ -55,9 +55,9 @@ async def async_copy2_safe(src, dst, log=None): like shutil.copy2, but log errors in copystat instead of raising """ - await run_sync_in_worker_thread(shutil.copyfile, src, dst) + await run_sync(shutil.copyfile, src, dst) try: - await run_sync_in_worker_thread(shutil.copystat, src, dst) + await run_sync(shutil.copystat, src, dst) except OSError: if log: log.debug("copystat on %s failed", dst, exc_info=True) @@ -355,7 +355,7 @@ async def _read_notebook(self, os_path, as_version=4): """Read a notebook from an os path.""" with self.open(os_path, 'r', encoding='utf-8') as f: try: - return await run_sync_in_worker_thread(partial(nbformat.read, as_version=as_version), f) + return await run_sync(partial(nbformat.read, as_version=as_version), f) except Exception as e: e_orig = e @@ -379,7 +379,7 @@ async def _read_notebook(self, os_path, as_version=4): async def _save_notebook(self, os_path, nb): """Save a notebook to an os_path.""" with self.atomic_writing(os_path, encoding='utf-8') as f: - await run_sync_in_worker_thread(partial(nbformat.write, version=nbformat.NO_CONVERT), nb, f) + await run_sync(partial(nbformat.write, version=nbformat.NO_CONVERT), nb, f) async def _read_file(self, os_path, format): """Read a non-notebook file. @@ -394,7 +394,7 @@ async def _read_file(self, os_path, format): raise HTTPError(400, "Cannot read non-file %s" % os_path) with self.open(os_path, 'rb') as f: - bcontent = await run_sync_in_worker_thread(f.read) + bcontent = await run_sync(f.read) if format is None or format == 'text': # Try to interpret as unicode if format is unknown or if unicode @@ -429,4 +429,4 @@ async def _save_file(self, os_path, content, format): ) from e with self.atomic_writing(os_path, text=False) as f: - await run_sync_in_worker_thread(f.write, bcontent) + await run_sync(f.write, bcontent) diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 5166969427..1ceaaefb62 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -12,7 +12,7 @@ import mimetypes import nbformat -from anyio import run_sync_in_worker_thread +from anyio.to_thread import run_sync from send2trash import send2trash from tornado import web @@ -578,7 +578,7 @@ async def _dir_model(self, path, content=True): if content: model['content'] = contents = [] os_dir = self._get_os_path(path) - dir_contents = await run_sync_in_worker_thread(os.listdir, os_dir) + dir_contents = await run_sync(os.listdir, os_dir) for name in dir_contents: try: os_path = os.path.join(os_dir, name) @@ -588,7 +588,7 @@ async def _dir_model(self, path, content=True): continue try: - st = await run_sync_in_worker_thread(os.lstat, os_path) + st = await run_sync(os.lstat, os_path) except OSError as e: # skip over broken symlinks in listing if e.errno == errno.ENOENT: @@ -721,7 +721,7 @@ async def _save_directory(self, os_path, model, path=''): raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path) if not os.path.exists(os_path): with self.perm_to_403(): - await run_sync_in_worker_thread(os.mkdir, os_path) + await run_sync(os.mkdir, os_path) elif not os.path.isdir(os_path): raise web.HTTPError(400, u'Not a directory: %s' % (os_path)) else: @@ -791,8 +791,8 @@ async def _check_trash(os_path): # It's a bit more nuanced than this, but until we can better # distinguish errors from send2trash, assume that we can only trash # files on the same partition as the home directory. - file_dev = (await run_sync_in_worker_thread(os.stat, os_path)).st_dev - home_dev = (await run_sync_in_worker_thread(os.stat, os.path.expanduser('~'))).st_dev + file_dev = (await run_sync(os.stat, os_path)).st_dev + home_dev = (await run_sync(os.stat, os.path.expanduser('~'))).st_dev return file_dev == home_dev async def is_non_empty_dir(os_path): @@ -800,7 +800,7 @@ async def is_non_empty_dir(os_path): # A directory containing only leftover checkpoints is # considered empty. cp_dir = getattr(self.checkpoints, 'checkpoint_dir', None) - dir_contents = set(await run_sync_in_worker_thread(os.listdir, os_path)) + dir_contents = set(await run_sync(os.listdir, os_path)) if dir_contents - {cp_dir}: return True @@ -828,11 +828,11 @@ async def is_non_empty_dir(os_path): raise web.HTTPError(400, u'Directory %s not empty' % os_path) self.log.debug("Removing directory %s", os_path) with self.perm_to_403(): - await run_sync_in_worker_thread(shutil.rmtree, os_path) + await run_sync(shutil.rmtree, os_path) else: self.log.debug("Unlinking file %s", os_path) with self.perm_to_403(): - await run_sync_in_worker_thread(rm, os_path) + await run_sync(rm, os_path) async def rename_file(self, old_path, new_path): """Rename a file.""" @@ -851,7 +851,7 @@ async def rename_file(self, old_path, new_path): # Move the file try: with self.perm_to_403(): - await run_sync_in_worker_thread(shutil.move, old_os_path, new_os_path) + await run_sync(shutil.move, old_os_path, new_os_path) except web.HTTPError: raise except Exception as e: diff --git a/jupyter_server/services/contents/largefilemanager.py b/jupyter_server/services/contents/largefilemanager.py index b2c7a2fd74..936fc5cfbb 100644 --- a/jupyter_server/services/contents/largefilemanager.py +++ b/jupyter_server/services/contents/largefilemanager.py @@ -1,4 +1,4 @@ -from anyio import run_sync_in_worker_thread +from anyio.to_thread import run_sync from tornado import web import base64 import os, io @@ -135,6 +135,6 @@ async def _save_large_file(self, os_path, content, format): 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: - await run_sync_in_worker_thread(f.write, bcontent) + await run_sync(f.write, bcontent) diff --git a/jupyter_server/tests/services/contents/test_manager.py b/jupyter_server/tests/services/contents/test_manager.py index 98f358645c..4c13cabee3 100644 --- a/jupyter_server/tests/services/contents/test_manager.py +++ b/jupyter_server/tests/services/contents/test_manager.py @@ -23,7 +23,7 @@ def jp_contents_manager(request, tmp_path): @pytest.fixture(params=[FileContentsManager, AsyncFileContentsManager]) -def file_contents_manager_class(request, tmp_path): +def jp_file_contents_manager_class(request, tmp_path): return request.param # -------------- Functions ---------------------------- @@ -100,46 +100,45 @@ async def check_populated_dir_files(jp_contents_manager, api_path): # ----------------- Tests ---------------------------------- -def test_root_dir(file_contents_manager_class, tmp_path): - fm = file_contents_manager_class(root_dir=str(tmp_path)) +def test_root_dir(jp_file_contents_manager_class, tmp_path): + fm = jp_file_contents_manager_class(root_dir=str(tmp_path)) assert fm.root_dir == str(tmp_path) -def test_missing_root_dir(file_contents_manager_class, tmp_path): +def test_missing_root_dir(jp_file_contents_manager_class, tmp_path): root = tmp_path / 'notebook' / 'dir' / 'is' / 'missing' with pytest.raises(TraitError): - file_contents_manager_class(root_dir=str(root)) + jp_file_contents_manager_class(root_dir=str(root)) -def test_invalid_root_dir(file_contents_manager_class, tmp_path): +def test_invalid_root_dir(jp_file_contents_manager_class, tmp_path): temp_file = tmp_path / 'file.txt' temp_file.write_text('') with pytest.raises(TraitError): - file_contents_manager_class(root_dir=str(temp_file)) + jp_file_contents_manager_class(root_dir=str(temp_file)) - -def test_get_os_path(file_contents_manager_class, tmp_path): - fm = file_contents_manager_class(root_dir=str(tmp_path)) +def test_get_os_path(jp_file_contents_manager_class, tmp_path): + fm = jp_file_contents_manager_class(root_dir=str(tmp_path)) path = fm._get_os_path('/path/to/notebook/test.ipynb') rel_path_list = '/path/to/notebook/test.ipynb'.split('/') fs_path = os.path.join(fm.root_dir, *rel_path_list) assert path == fs_path - fm = file_contents_manager_class(root_dir=str(tmp_path)) + fm = jp_file_contents_manager_class(root_dir=str(tmp_path)) path = fm._get_os_path('test.ipynb') fs_path = os.path.join(fm.root_dir, 'test.ipynb') assert path == fs_path - fm = file_contents_manager_class(root_dir=str(tmp_path)) + fm = jp_file_contents_manager_class(root_dir=str(tmp_path)) path = fm._get_os_path('////test.ipynb') fs_path = os.path.join(fm.root_dir, 'test.ipynb') assert path == fs_path -def test_checkpoint_subdir(file_contents_manager_class, tmp_path): +def test_checkpoint_subdir(jp_file_contents_manager_class, tmp_path): subd = 'sub ∂ir' cp_name = 'test-cp.ipynb' - fm = file_contents_manager_class(root_dir=str(tmp_path)) + fm = jp_file_contents_manager_class(root_dir=str(tmp_path)) tmp_path.joinpath(subd).mkdir() cpm = fm.checkpoints cp_dir = cpm.checkpoint_path('cp', 'test.ipynb') @@ -148,10 +147,10 @@ def test_checkpoint_subdir(file_contents_manager_class, tmp_path): assert cp_dir == os.path.join(str(tmp_path), cpm.checkpoint_dir, cp_name) -async def test_bad_symlink(file_contents_manager_class, tmp_path): +async def test_bad_symlink(jp_file_contents_manager_class, tmp_path): td = str(tmp_path) - cm = file_contents_manager_class(root_dir=td) + cm = jp_file_contents_manager_class(root_dir=td) path = 'test bad symlink' _make_dir(cm, path) @@ -173,10 +172,10 @@ async def test_bad_symlink(file_contents_manager_class, tmp_path): sys.platform.startswith('win'), reason="Windows doesn't detect symlink loops" ) -async def test_recursive_symlink(file_contents_manager_class, tmp_path): +async def test_recursive_symlink(jp_file_contents_manager_class, tmp_path): td = str(tmp_path) - cm = file_contents_manager_class(root_dir=td) + cm = jp_file_contents_manager_class(root_dir=td) path = 'test recursive symlink' _make_dir(cm, path) @@ -195,9 +194,9 @@ async def test_recursive_symlink(file_contents_manager_class, tmp_path): assert 'recursive' not in contents -async def test_good_symlink(file_contents_manager_class, tmp_path): +async def test_good_symlink(jp_file_contents_manager_class, tmp_path): td = str(tmp_path) - cm = file_contents_manager_class(root_dir=td) + cm = jp_file_contents_manager_class(root_dir=td) parent = 'test good symlink' name = 'good symlink' path = '{0}/{1}'.format(parent, name) @@ -216,13 +215,13 @@ async def test_good_symlink(file_contents_manager_class, tmp_path): sys.platform.startswith('win'), reason="Can't test permissions on Windows" ) -async def test_403(file_contents_manager_class, tmp_path): +async def test_403(jp_file_contents_manager_class, tmp_path): if hasattr(os, 'getuid'): if os.getuid() == 0: raise pytest.skip("Can't test permissions as root") td = str(tmp_path) - cm = file_contents_manager_class(root_dir=td) + cm = jp_file_contents_manager_class(root_dir=td) model = await ensure_async(cm.new_untitled(type='file')) os_path = cm._get_os_path(model['path']) @@ -233,10 +232,9 @@ async def test_403(file_contents_manager_class, tmp_path): except HTTPError as e: assert e.status_code == 403 - -async def test_escape_root(file_contents_manager_class, tmp_path): +async def test_escape_root(jp_file_contents_manager_class, tmp_path): td = str(tmp_path) - cm = file_contents_manager_class(root_dir=td) + cm = jp_file_contents_manager_class(root_dir=td) # make foo, bar next to root with open(os.path.join(cm.root_dir, '..', 'foo'), 'w') as f: f.write('foo') diff --git a/setup.cfg b/setup.cfg index be43c8617b..c9a77eb785 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ install_requires = terminado>=0.8.3 prometheus_client pywin32>=1.0 ; sys_platform == 'win32' - anyio>=2.0.2,<3 + anyio>=3.0.1,<4 [options.extras_require] test = coverage; pytest; pytest-cov; pytest-mock; requests; pytest-tornasync; pytest-console-scripts; ipykernel From eff9b0a82f56720e2955da4acde456dbf879cd82 Mon Sep 17 00:00:00 2001 From: Mariko Wakabayashi Date: Sun, 2 May 2021 20:44:14 -0600 Subject: [PATCH 7/7] Cap anyio < 3 for python < 3.7 --- jupyter_server/services/contents/filecheckpoints.py | 7 ++++++- jupyter_server/services/contents/fileio.py | 7 ++++++- jupyter_server/services/contents/filemanager.py | 7 ++++++- jupyter_server/services/contents/largefilemanager.py | 7 ++++++- setup.cfg | 3 ++- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/jupyter_server/services/contents/filecheckpoints.py b/jupyter_server/services/contents/filecheckpoints.py index 5c54f0f360..49280d770f 100644 --- a/jupyter_server/services/contents/filecheckpoints.py +++ b/jupyter_server/services/contents/filecheckpoints.py @@ -14,7 +14,12 @@ ) from .fileio import AsyncFileManagerMixin, FileManagerMixin -from anyio.to_thread import run_sync +try: + from anyio.to_thread import run_sync +except ImportError: + # fallback on anyio v2 for python version < 3.7 + from anyio import run_sync_in_worker_thread as run_sync + from jupyter_core.utils import ensure_dir_exists from traitlets import Unicode diff --git a/jupyter_server/services/contents/fileio.py b/jupyter_server/services/contents/fileio.py index 559139fdf0..dc6e0ef764 100644 --- a/jupyter_server/services/contents/fileio.py +++ b/jupyter_server/services/contents/fileio.py @@ -12,7 +12,12 @@ import os import shutil -from anyio.to_thread import run_sync +try: + from anyio.to_thread import run_sync +except ImportError: + # fallback on anyio v2 for python version < 3.7 + from anyio import run_sync_in_worker_thread as run_sync + from tornado.web import HTTPError from jupyter_server.utils import ( diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 1ceaaefb62..15bcb433d1 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -12,7 +12,12 @@ import mimetypes import nbformat -from anyio.to_thread import run_sync +try: + from anyio.to_thread import run_sync +except ImportError: + # fallback on anyio v2 for python version < 3.7 + from anyio import run_sync_in_worker_thread as run_sync + from send2trash import send2trash from tornado import web diff --git a/jupyter_server/services/contents/largefilemanager.py b/jupyter_server/services/contents/largefilemanager.py index 936fc5cfbb..e10cd5d94a 100644 --- a/jupyter_server/services/contents/largefilemanager.py +++ b/jupyter_server/services/contents/largefilemanager.py @@ -1,4 +1,9 @@ -from anyio.to_thread import run_sync +try: + from anyio.to_thread import run_sync +except ImportError: + # fallback on anyio v2 for python version < 3.7 + from anyio import run_sync_in_worker_thread as run_sync + from tornado import web import base64 import os, io diff --git a/setup.cfg b/setup.cfg index c9a77eb785..4cd951aca2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,8 @@ install_requires = terminado>=0.8.3 prometheus_client pywin32>=1.0 ; sys_platform == 'win32' - anyio>=3.0.1,<4 + anyio>=2.0.2,<3 ; python_version < '3.7' + anyio>=3.0.1,<4 ; python_version >= '3.7' [options.extras_require] test = coverage; pytest; pytest-cov; pytest-mock; requests; pytest-tornasync; pytest-console-scripts; ipykernel