Repository: jupyter/jupyter_core

Issue:
Feature request: do not prefer env path if it is not writeable
Jupyter_core 5.0.0 made the env path preferred (unless you set `JUPYTER_PREFER_ENV_PATH=no`).  This is probably the right general case, but it didn't work for us, and here's why.

We are indeed running that lab from inside an environment (specifically rubin-env-rsp from conda-forge)...but the way we have the Rubin Science Platform constructed, that path is not writeable.  It's read-only within the container.

I suspect you didn't consider that case; it is after all pretty unusual.  I feel like a better default would be to see whether the env path is writeable, and if it is NOT, do not prefer it, but if it is, then prefer it.  This would give the advantages of the 5.0 strategy, but also accommodate our use-case and that of sites doing the same thing we are of providing a user-immutable reference platform.

I don't know if I will have the cycles myself to produce a PR that will implement this, but if I do, I will.

Discussion:
minrk: This now has an ownership check, which falls back on W_OK if no owner can be established, which I _think_ will happen on Windows.
blink1073: @jasongrout had brought up some concerns about special-casing the "writability" of the path [here](https://github.com/jupyter/jupyter_core/issues/318#issuecomment-1330543591).
minrk: @jasongrout thanks! I left comments in #318. I think shared env detection is too complex and fraught with wrong conclusions (e.g. admin vs user getting different results for the same env). I think the solution probably belongs in checks in jupyterlab  whatever package is choosing to require writing. I do believe it shouldn't be on the user to solve, though, for standard persistence behavior like jupyterlab workspaces.
minrk: Reopening now that it's switched to an ownership check, following discussion in #318

Patch:
diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py
index 708cbac..b801f8c 100644
--- a/jupyter_core/paths.py
+++ b/jupyter_core/paths.py
@@ -68,6 +68,33 @@ def get_home_dir() -> str:
 _dtemps: Dict[str, str] = {}
 
 
+def _do_i_own(path: str) -> bool:
+    """Return whether the current user owns the given path"""
+    p = Path(path).resolve()
+
+    # walk up to first existing parent
+    while not p.exists() and p != p.parent:
+        p = p.parent
+
+    # simplest check: owner by name
+    # not always implemented or available
+    try:
+        return p.owner() == os.getlogin()
+    except (NotImplementedError, OSError):
+        pass
+
+    if hasattr(os, 'geteuid'):
+        try:
+            st = p.stat()
+            return st.st_uid == os.geteuid()
+        except (NotImplementedError, OSError):
+            # geteuid not always implemented
+            pass
+
+    # no ownership checks worked, check write access
+    return os.access(p, os.W_OK)
+
+
 def prefer_environment_over_user() -> bool:
     """Determine if environment-level paths should take precedence over user-level paths."""
     # If JUPYTER_PREFER_ENV_PATH is defined, that signals user intent, so return its value
@@ -75,7 +102,7 @@ def prefer_environment_over_user() -> bool:
         return envset("JUPYTER_PREFER_ENV_PATH")  # type:ignore[return-value]
 
     # If we are in a Python virtualenv, default to True (see https://docs.python.org/3/library/venv.html#venv-def)
-    if sys.prefix != sys.base_prefix:
+    if sys.prefix != sys.base_prefix and _do_i_own(sys.prefix):
         return True
 
     # If sys.prefix indicates Python comes from a conda/mamba environment that is not the root environment, default to True
@@ -83,6 +110,7 @@ def prefer_environment_over_user() -> bool:
         "CONDA_PREFIX" in os.environ
         and sys.prefix.startswith(os.environ["CONDA_PREFIX"])
         and os.environ.get("CONDA_DEFAULT_ENV", "base") != "base"
+        and _do_i_own(sys.prefix)
     ):
         return True
 


Test patch:
diff --git a/jupyter_core/tests/test_paths.py b/jupyter_core/tests/test_paths.py
index 2f40d6e..d7d48e9 100644
--- a/jupyter_core/tests/test_paths.py
+++ b/jupyter_core/tests/test_paths.py
@@ -400,9 +400,10 @@ def test_prefer_environment_over_user():
 
     # Test default if environment variable is not set, and try to determine if we are in a virtual environment
     os.environ.pop("JUPYTER_PREFER_ENV_PATH", None)
+
     # base prefix differs, venv
     with patch.object(sys, "base_prefix", "notthesame"):
-        assert prefer_environment_over_user()
+        assert prefer_environment_over_user() == paths._do_i_own(sys.prefix)
 
     # conda
     with patch.object(sys, "base_prefix", sys.prefix):
@@ -411,7 +412,8 @@ def test_prefer_environment_over_user():
             assert not prefer_environment_over_user()
         # in non-base env, prefer it
         with patch.dict(os.environ, {"CONDA_PREFIX": sys.prefix, "CONDA_DEFAULT_ENV": "/tmp"}):
-            assert prefer_environment_over_user()
+            assert prefer_environment_over_user() == paths._do_i_own(sys.prefix)
+
         # conda env defined, but we aren't using it
         with patch.dict(
             os.environ, {"CONDA_PREFIX": "/somewherelese", "CONDA_DEFAULT_ENV": "/tmp"}