Skip to content

Commit 5e89656

Browse files
authored
Merge commit from fork
* %PROGRAMDATA% is not safe by default on Windows, require opt-in require setting JUPYTER_USE_PROGRAMDATA=1 in order to look here for system-wide config # Conflicts: # jupyter_core/paths.py * test JUPYTER_USE_PROGRAMDATA on Windows * make sure new Windows default env == system doesn't mess with prefer_env ordering * document JUPYTER_USE_PROGRAMDATA change * more jupyter_path docstring
1 parent 23150c0 commit 5e89656

File tree

2 files changed

+120
-19
lines changed

2 files changed

+120
-19
lines changed

jupyter_core/paths.py

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,22 @@ def jupyter_runtime_dir() -> str:
210210
return pjoin(jupyter_data_dir(), "runtime")
211211

212212

213+
# %PROGRAMDATA% is not safe by default, require opt-in to trust it
214+
_use_programdata: bool = envset("JUPYTER_USE_PROGRAMDATA")
215+
# _win_programdata is a path str if we're using it, None otherwise
216+
_win_programdata: str | None = None
217+
if os.name == "nt" and _use_programdata:
218+
_win_programdata = os.environ.get("PROGRAMDATA", None)
219+
220+
213221
if use_platform_dirs():
214-
SYSTEM_JUPYTER_PATH = platformdirs.site_data_dir(
215-
APPNAME, appauthor=False, multipath=True
216-
).split(os.pathsep)
222+
if os.name == "nt" and not _use_programdata:
223+
# default PROGRAMDATA used by site_* is not safe by default on Windows
224+
SYSTEM_JUPYTER_PATH = [str(Path(sys.prefix, "share", "jupyter"))]
225+
else:
226+
SYSTEM_JUPYTER_PATH = platformdirs.site_data_dir(
227+
APPNAME, appauthor=False, multipath=True
228+
).split(os.pathsep)
217229
else:
218230
deprecation(
219231
"Jupyter is migrating its paths to use standard platformdirs\n"
@@ -223,10 +235,10 @@ def jupyter_runtime_dir() -> str:
223235
"The use of platformdirs will be the default in `jupyter_core` v6"
224236
)
225237
if os.name == "nt":
226-
programdata = os.environ.get("PROGRAMDATA", None)
227-
if programdata:
228-
SYSTEM_JUPYTER_PATH = [pjoin(programdata, "jupyter")]
229-
else: # PROGRAMDATA is not defined by default on XP.
238+
# PROGRAMDATA is not defined by default on XP, and not safe by default
239+
if _win_programdata:
240+
SYSTEM_JUPYTER_PATH = [pjoin(_win_programdata, "jupyter")]
241+
else:
230242
SYSTEM_JUPYTER_PATH = [str(Path(sys.prefix, "share", "jupyter"))]
231243
else:
232244
SYSTEM_JUPYTER_PATH = [
@@ -238,19 +250,40 @@ def jupyter_runtime_dir() -> str:
238250

239251

240252
def jupyter_path(*subdirs: str) -> list[str]:
241-
"""Return a list of directories to search for data files
253+
"""Return a list of directories to search for data files.
254+
255+
There are four sources of paths to search:
256+
257+
- $JUPYTER_PATH environment variable (always highest priority)
258+
- user directories (e.g. ~/.local/share/jupyter)
259+
- environment directories (e.g. {sys.prefix}/share/jupyter)
260+
- system-wide paths (e.g. /usr/local/share/jupyter)
242261
243-
JUPYTER_PATH environment variable has highest priority.
262+
JUPYTER_PATH environment variable has highest priority, if defined,
263+
and is purely additive.
244264
245265
If the JUPYTER_PREFER_ENV_PATH environment variable is set, the environment-level
246266
directories will have priority over user-level directories.
267+
You can also set JUPYTER_PREFER_ENV_PATH=0 to explicitly prefer user directories.
268+
If Jupyter detects that you are in a virtualenv or conda environment,
269+
environment paths are also preferred to user paths,
270+
otherwise user paths are preferred to environment paths.
247271
248272
If the Python site.ENABLE_USER_SITE variable is True, we also add the
249273
appropriate Python user site subdirectory to the user-level directories.
250274
275+
Finally, system-wide directories, such as `/usr/local/share/jupyter` are searched.
251276
252277
If ``*subdirs`` are given, that subdirectory will be added to each element.
253278
279+
280+
.. versionchanged:: 5.8
281+
282+
On Windows, %PROGRAMDATA% will be used as a system-wide path only if
283+
the JUPYTER_USE_PROGRAMDATA env is set.
284+
By default, there is no default system-wide path on Windows and the env path
285+
is used instead.
286+
254287
Examples:
255288
256289
>>> jupyter_path()
@@ -278,7 +311,13 @@ def jupyter_path(*subdirs: str) -> list[str]:
278311
if userdir not in user:
279312
user.append(userdir)
280313

281-
env = [p for p in ENV_JUPYTER_PATH if p not in SYSTEM_JUPYTER_PATH]
314+
# Windows usually doesn't have a 'system' prefix,
315+
# so 'system' and 'env' are the same
316+
# make sure that env can still be preferred in this case
317+
if ENV_JUPYTER_PATH == SYSTEM_JUPYTER_PATH:
318+
env = ENV_JUPYTER_PATH
319+
else:
320+
env = [p for p in ENV_JUPYTER_PATH if p not in SYSTEM_JUPYTER_PATH]
282321

283322
if prefer_environment_over_user():
284323
paths.extend(env)
@@ -287,8 +326,10 @@ def jupyter_path(*subdirs: str) -> list[str]:
287326
paths.extend(user)
288327
paths.extend(env)
289328

290-
# finally, system
291-
paths.extend(SYSTEM_JUPYTER_PATH)
329+
# finally, add system paths (can overlap with env, so avoid duplicates)
330+
for _path in SYSTEM_JUPYTER_PATH:
331+
if _path not in paths:
332+
paths.append(_path)
292333

293334
# add subdir, if requested
294335
if subdirs:
@@ -297,14 +338,18 @@ def jupyter_path(*subdirs: str) -> list[str]:
297338

298339

299340
if use_platform_dirs():
300-
SYSTEM_CONFIG_PATH = platformdirs.site_config_dir(
301-
APPNAME, appauthor=False, multipath=True
302-
).split(os.pathsep)
341+
if os.name == "nt" and not _use_programdata:
342+
# default PROGRAMDATA is not safe by default on Windows
343+
SYSTEM_CONFIG_PATH = []
344+
else:
345+
SYSTEM_CONFIG_PATH = platformdirs.site_config_dir(
346+
APPNAME, appauthor=False, multipath=True
347+
).split(os.pathsep)
303348
elif os.name == "nt":
304-
programdata = os.environ.get("PROGRAMDATA", None)
305-
if programdata:
306-
SYSTEM_CONFIG_PATH = [str(Path(programdata, "jupyter"))]
307-
else: # PROGRAMDATA is not defined by default on XP.
349+
# PROGRAMDATA is not defined by default on XP, and not safe by default
350+
if _win_programdata:
351+
SYSTEM_CONFIG_PATH = [str(Path(_win_programdata, "jupyter"))]
352+
else:
308353
SYSTEM_CONFIG_PATH = []
309354
else:
310355
SYSTEM_CONFIG_PATH = [
@@ -323,6 +368,21 @@ def jupyter_config_path() -> list[str]:
323368
324369
If the Python site.ENABLE_USER_SITE variable is True, we also add the
325370
appropriate Python user site subdirectory to the user-level directories.
371+
372+
Finally, system-wide directories such as `/usr/local/etc/jupyter` are searched.
373+
374+
375+
.. versionchanged:: 5.8
376+
377+
On Windows, %PROGRAMDATA% will be used as a system-wide path only if
378+
the JUPYTER_USE_PROGRAMDATA env is set.
379+
By default, there is no system-wide config path on Windows.
380+
381+
Examples:
382+
383+
>>> jupyter_config_path()
384+
['~/.jupyter', '~/.local/etc/jupyter', '/usr/local/etc/jupyter', '/etc/jupyter']
385+
326386
"""
327387
if os.environ.get("JUPYTER_NO_CONFIG"):
328388
# jupyter_config_dir makes a blank config when JUPYTER_NO_CONFIG is set.

tests/test_paths.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
import sys
1414
import tempfile
1515
import warnings
16+
from importlib import reload
17+
from pathlib import Path
1618
from unittest.mock import patch
1719

20+
import platformdirs
1821
import pytest
1922
from platformdirs import __version_info__
2023

@@ -72,6 +75,7 @@ def setup_function():
7275
"JUPYTER_PATH",
7376
"JUPYTER_PLATFORM_DIRS",
7477
"JUPYTER_RUNTIME_DIR",
78+
"JUPYTER_USE_PROGRAMDATA",
7579
]:
7680
os.environ.pop(var, None)
7781
# For these tests, default to preferring the user-level over environment-level paths
@@ -598,3 +602,40 @@ def test_insecure_write_warning():
598602
with warnings.catch_warnings():
599603
warnings.simplefilter("ignore", UserWarning)
600604
issue_insecure_write_warning()
605+
606+
607+
@windows
608+
@pytest.mark.parametrize("use_programdata", ["1", "0", None])
609+
@pytest.mark.parametrize("use_platformdirs", ["1", "0"])
610+
def test_windows_programdata(request, tmp_path, use_programdata, use_platformdirs):
611+
request.addfinalizer(lambda: reload(paths))
612+
programdata = tmp_path / "programdata"
613+
env = {
614+
"PROGRAMDATA": str(programdata),
615+
"JUPYTER_PLATFORM_DIRS": use_platformdirs,
616+
}
617+
if use_programdata is not None:
618+
env["JUPYTER_USE_PROGRAMDATA"] = use_programdata
619+
with patch.dict(os.environ, env):
620+
reload(paths)
621+
# SIM300 (yoda conditions) gets false positives
622+
# when the 'variable' we are testing looks like a constant
623+
if use_programdata in {"0", None}:
624+
assert paths.SYSTEM_CONFIG_PATH == []
625+
assert paths.SYSTEM_JUPYTER_PATH == [str(Path(sys.prefix, "share", "jupyter"))] # noqa:SIM300
626+
# use_programdata is True
627+
elif use_platformdirs == "1":
628+
assert paths.SYSTEM_CONFIG_PATH == ( # noqa:SIM300
629+
platformdirs.site_config_dir(paths.APPNAME, appauthor=False, multipath=True).split(
630+
os.pathsep
631+
)
632+
)
633+
634+
assert paths.SYSTEM_JUPYTER_PATH == ( # noqa:SIM300
635+
platformdirs.site_data_dir(paths.APPNAME, appauthor=False, multipath=True).split(
636+
os.pathsep
637+
)
638+
)
639+
else:
640+
assert paths.SYSTEM_CONFIG_PATH == [str(programdata / "jupyter")] # noqa:SIM300
641+
assert paths.SYSTEM_JUPYTER_PATH == [str(programdata / "jupyter")] # noqa:SIM300

0 commit comments

Comments
 (0)