diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 67bef785e7c..4548453d49a 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -4,9 +4,9 @@ ; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme ; [subrepo] - remote = https://github.com/spyder-ide/spyder-kernels.git - branch = master - commit = f9fc2f62179a98cf582b44306a9f9225506ec532 - parent = 3d037175c3ae4bb20f1366a6e5446ae064ed6306 + remote = https://github.com/mrclary/spyder-kernels.git + branch = ppm-syspath + commit = 9babbc72ff531504c93a52a41e963f203ac6716a + parent = 855143290575418e0f3ad5d9100e8e3c482389a8 method = merge cmdver = 0.4.9 diff --git a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py index ad16c08475a..07c3927d66a 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/kernel.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/kernel.py @@ -92,6 +92,11 @@ def __init__(self, *args, **kwargs): # To save the python env info self.pythonenv_info: PythonEnvInfo = {} + # Store original sys.path. Kernels are started with PYTHONPATH + # removed from environment variables, so this will never have + # user paths and should be clean. + self._sys_path = sys.path.copy() + @property def kernel_info(self): # Used for checking correct version by spyder @@ -744,27 +749,43 @@ def set_special_kernel(self, special): raise NotImplementedError(f"{special}") @comm_handler - def update_syspath(self, path_dict, new_path_dict): + def update_syspath(self, new_path, prioritize): """ Update the PYTHONPATH of the kernel. - `path_dict` and `new_path_dict` have the paths as keys and the state - as values. The state is `True` for active and `False` for inactive. - - `path_dict` corresponds to the previous state of the PYTHONPATH. - `new_path_dict` corresponds to the new state of the PYTHONPATH. + Parameters + ---------- + new_path: list of str + List of PYTHONPATH paths. + prioritize: bool + Whether to place PYTHONPATH paths at the front (True) or + back (False) of sys.path. + + + Notes + ----- + A copy of sys.path is made at instantiation, which should be clean, + so we can just prepend/append to the copy without having to explicitly + remove old user paths. PYTHONPATH can just be overwritten. """ - # Remove old paths - for path in path_dict: - while path in sys.path: - sys.path.remove(path) - - # Add new paths - pypath = [path for path, active in new_path_dict.items() if active] - if pypath: - sys.path.extend(pypath) - os.environ.update({'PYTHONPATH': os.pathsep.join(pypath)}) + if new_path is not None: + # Overwrite PYTHONPATH + os.environ.update({'PYTHONPATH': os.pathsep.join(new_path)}) + + # Add new paths to original sys.path + if prioritize: + sys.path[:] = new_path + self._sys_path + + # Ensure current directory is always first to imitate Python + # standard behavior + if '' in sys.path: + sys.path.remove('') + sys.path.insert(0, '') + else: + sys.path[:] = self._sys_path + new_path else: + # Restore original sys.path and remove PYTHONPATH + sys.path[:] = self._sys_path os.environ.pop('PYTHONPATH', None) @comm_handler diff --git a/external-deps/spyder-kernels/spyder_kernels/console/start.py b/external-deps/spyder-kernels/spyder_kernels/console/start.py index 600b656c02e..ce1440e2b76 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/start.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/start.py @@ -16,6 +16,14 @@ import sys import site +# Remove current directory from sys.path to prevent kernel crashes when people +# name Python files or modules with the same name as standard library modules. +# See spyder-ide/spyder#8007 +# Inject it back into sys.path after all imports in this module but +# before the kernel is initialized +while '' in sys.path: + sys.path.remove('') + # Third-party imports from traitlets import DottedObjectName @@ -29,13 +37,6 @@ def import_spydercustomize(): parent = osp.dirname(here) customize_dir = osp.join(parent, 'customize') - # Remove current directory from sys.path to prevent kernel - # crashes when people name Python files or modules with - # the same name as standard library modules. - # See spyder-ide/spyder#8007 - while '' in sys.path: - sys.path.remove('') - # Import our customizations site.addsitedir(customize_dir) import spydercustomize # noqa @@ -46,6 +47,7 @@ def import_spydercustomize(): except ValueError: pass + def kernel_config(): """Create a config object with IPython kernel options.""" from IPython.core.application import get_ipython_dir @@ -153,13 +155,6 @@ def main(): # Import our customizations into the kernel import_spydercustomize() - # Remove current directory from sys.path to prevent kernel - # crashes when people name Python files or modules with - # the same name as standard library modules. - # See spyder-ide/spyder#8007 - while '' in sys.path: - sys.path.remove('') - # Main imports from ipykernel.kernelapp import IPKernelApp from spyder_kernels.console.kernel import SpyderKernel @@ -192,6 +187,12 @@ def close(self): kernel.config = kernel_config() except: pass + + # Re-add current working directory path into sys.path after all of the + # import statements, but before initializing the kernel. + if '' not in sys.path: + sys.path.insert(0, '') + kernel.initialize() # Set our own magics diff --git a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py index f6f58c39b61..ba748dcdb4e 100644 --- a/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py +++ b/external-deps/spyder-kernels/spyder_kernels/console/tests/test_console_kernel.py @@ -77,13 +77,15 @@ def setup_kernel(cmd): ) # wait for connection file to exist, timeout after 5s tic = time.time() - while not os.path.exists(connection_file) \ - and kernel.poll() is None \ - and time.time() < tic + SETUP_TIMEOUT: + while ( + not os.path.exists(connection_file) + and kernel.poll() is None + and time.time() < tic + SETUP_TIMEOUT + ): time.sleep(0.1) if kernel.poll() is not None: - o,e = kernel.communicate() + o, e = kernel.communicate() raise IOError("Kernel failed to start:\n%s" % e) if not os.path.exists(connection_file): @@ -229,7 +231,7 @@ def kernel(request): 'True_' ], 'minmax': False, - 'filter_on':True + 'filter_on': True } # Teardown @@ -468,8 +470,11 @@ def test_is_defined(kernel): def test_get_doc(kernel): """Test to get object documentation dictionary.""" objtxt = 'help' - assert ("Define the builtin 'help'" in kernel.get_doc(objtxt)['docstring'] or - "Define the built-in 'help'" in kernel.get_doc(objtxt)['docstring']) + assert ( + "Define the builtin 'help'" in kernel.get_doc(objtxt)['docstring'] + or "Define the built-in 'help'" in kernel.get_doc(objtxt)['docstring'] + ) + def test_get_source(kernel): """Test to get object source.""" @@ -507,7 +512,7 @@ def test_cwd_in_sys_path(): with setup_kernel(cmd) as client: reply = client.execute_interactive( "import sys; sys_path = sys.path", - user_expressions={'output':'sys_path'}, timeout=TIMEOUT) + user_expressions={'output': 'sys_path'}, timeout=TIMEOUT) # Transform value obtained through user_expressions user_expressions = reply['content']['user_expressions'] @@ -518,6 +523,21 @@ def test_cwd_in_sys_path(): assert '' in value +def test_prioritize(kernel): + """Test that user path priority is honored in sys.path.""" + syspath = kernel.get_syspath() + append_path = ['/test/append/path'] + prepend_path = ['/test/prepend/path'] + + kernel.update_syspath(append_path, prioritize=False) + new_syspath = kernel.get_syspath() + assert new_syspath == syspath + append_path + + kernel.update_syspath(prepend_path, prioritize=True) + new_syspath = kernel.get_syspath() + assert new_syspath == prepend_path + syspath + + @flaky(max_runs=3) def test_multiprocessing(tmpdir): """ @@ -701,8 +721,10 @@ def test_runfile(tmpdir): assert content['found'] # Run code file `u` with current namespace - msg = client.execute_interactive("%runfile {} --current-namespace" - .format(repr(str(u))), timeout=TIMEOUT) + msg = client.execute_interactive( + "%runfile {} --current-namespace".format(repr(str(u))), + timeout=TIMEOUT + ) content = msg['content'] # Verify that the variable `result3` is defined @@ -727,7 +749,9 @@ def test_runfile(tmpdir): sys.platform == 'darwin' and sys.version_info[:2] == (3, 8), reason="Fails on Mac with Python 3.8") def test_np_threshold(kernel): - """Test that setting Numpy threshold doesn't make the Variable Explorer slow.""" + """ + Test that setting Numpy threshold doesn't make the Variable Explorer slow. + """ cmd = "from spyder_kernels.console import start; start.main()" @@ -786,7 +810,9 @@ def test_np_threshold(kernel): while "data" not in msg['content']: msg = client.get_shell_msg(timeout=TIMEOUT) content = msg['content']['data']['text/plain'] - assert "{'float_kind':