Skip to content

Commit 93b6cce

Browse files
committed
Merge branch 'main' into allow_user_to_update_fields
2 parents 93385a9 + e12feb9 commit 93b6cce

File tree

6 files changed

+79
-4
lines changed

6 files changed

+79
-4
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@ If you are interested in contributing to the project, see [`CONTRIBUTING.rst`](C
4747
## Team Meetings and Roadmap
4848

4949
- When: Thursdays [8:00am, Pacific time](https://www.thetimezoneconverter.com/?t=8%3A00%20am&tz=San%20Francisco&)
50-
- Where: [Jovyan Zoom](https://zoom.us/my/jovyan?pwd=c0JZTHlNdS9Sek9vdzR3aTJ4SzFTQT09)
51-
- What: [Meeting notes](https://github.com/jupyter-server/team-compass/issues/45)
50+
- Where: [Jovyan Zoom](https://zoom.us/j/95228013874?pwd=Ep7HIk8t9JP6VToxt1Wj4P7K5PshC0.1)
51+
- What:
52+
- [Meeting notes](https://github.com/jupyter-server/team-compass/issues?q=is%3Aissue%20%20Meeting%20Notes%20)
53+
- [Agenda](https://hackmd.io/Wmz_wjrLRHuUbgWphjwRWw)
5254

5355
See our tentative [roadmap here](https://github.com/jupyter/jupyter_server/issues/127).
5456

jupyter_server/extension/serverextension.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,23 @@ def toggle_server_extension(self, import_name: str) -> None:
259259
self.log.info(f"- Writing config: {config_dir}")
260260
# Validate the server extension.
261261
self.log.info(f" - Validating {import_name}...")
262+
config = extension_manager.config_manager
263+
enabled = False
264+
if config:
265+
jpserver_extensions = config.get_jpserver_extensions()
266+
if import_name not in jpserver_extensions:
267+
msg = (
268+
f"The module '{import_name}' could not be found. Are you "
269+
"sure the extension is installed?"
270+
)
271+
raise ValueError(msg)
272+
enabled = jpserver_extensions[import_name]
273+
262274
# Interface with the Extension Package and validate.
263-
extpkg = ExtensionPackage(name=import_name)
264-
extpkg.validate()
275+
extpkg = ExtensionPackage(name=import_name, enabled=enabled)
276+
if not extpkg.validate():
277+
msg = "validation failed"
278+
raise ValueError(msg)
265279
version = extpkg.version
266280
self.log.info(f" {import_name} {version} {GREEN_OK}")
267281

jupyter_server/services/contents/filecheckpoints.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import shutil
7+
import tempfile
78

89
from anyio.to_thread import run_sync
910
from jupyter_core.utils import ensure_dir_exists
@@ -111,6 +112,10 @@ def checkpoint_path(self, checkpoint_id, path):
111112
filename = f"{basename}-{checkpoint_id}{ext}"
112113
os_path = self._get_os_path(path=parent)
113114
cp_dir = os.path.join(os_path, self.checkpoint_dir)
115+
# If parent directory isn't writable, use system temp
116+
if not os.access(os.path.dirname(cp_dir), os.W_OK):
117+
rel = os.path.relpath(os_path, start=self.root_dir)
118+
cp_dir = os.path.join(tempfile.gettempdir(), "jupyter_checkpoints", rel)
114119
with self.perm_to_403():
115120
ensure_dir_exists(cp_dir)
116121
cp_path = os.path.join(cp_dir, filename)

jupyter_server/services/contents/fileio.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def copy2_safe(src, dst, log=None):
3939
4040
like shutil.copy2, but log errors in copystat instead of raising
4141
"""
42+
# if src file is not writable, avoid creating a back-up
43+
if not os.access(src, os.W_OK):
44+
if log:
45+
log.debug("Source file, %s, is not writable", src, exc_info=True)
46+
raise PermissionError(errno.EACCES, f"File is not writable: {src}")
47+
4248
shutil.copyfile(src, dst)
4349
try:
4450
shutil.copystat(src, dst)
@@ -52,6 +58,11 @@ async def async_copy2_safe(src, dst, log=None):
5258
5359
like shutil.copy2, but log errors in copystat instead of raising
5460
"""
61+
if not os.access(src, os.W_OK):
62+
if log:
63+
log.debug("Source file, %s, is not writable", src, exc_info=True)
64+
raise PermissionError(errno.EACCES, f"File is not writable: {src}")
65+
5566
await run_sync(shutil.copyfile, src, dst)
5667
try:
5768
await run_sync(shutil.copystat, src, dst)
@@ -101,6 +112,21 @@ def atomic_writing(path, text=True, encoding="utf-8", log=None, **kwargs):
101112
if os.path.islink(path):
102113
path = os.path.join(os.path.dirname(path), os.readlink(path))
103114

115+
# Fall back to direct write for existing file in a non-writable dir
116+
dirpath = os.path.dirname(path) or os.getcwd()
117+
if os.path.isfile(path) and not os.access(dirpath, os.W_OK) and os.access(path, os.W_OK):
118+
mode = "w" if text else "wb"
119+
# direct open on the target file
120+
if text:
121+
fileobj = open(path, mode, encoding=encoding, **kwargs) # noqa: SIM115
122+
else:
123+
fileobj = open(path, mode, **kwargs) # noqa: SIM115
124+
try:
125+
yield fileobj
126+
finally:
127+
fileobj.close()
128+
return
129+
104130
tmp_path = path_to_intermediate(path)
105131

106132
if os.path.isfile(path):

tests/extension/test_serverextension.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,11 @@ def test_server_extension_apps(jp_env_config_path, jp_extension_environ):
134134
def test_server_extension_app():
135135
app = ServerExtensionApp()
136136
app.launch_instance(["list"])
137+
138+
139+
def test_toggle_missing_server_extension(jp_env_config_path, capsys):
140+
app = ToggleServerExtensionApp()
141+
app.extra_args = ["missingextension"]
142+
app.start()
143+
captured = capsys.readouterr()
144+
assert "Validation failed: The module 'missingextension' could not be found." in captured.err

tests/services/contents/test_fileio.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,26 @@ def test_path_to_invalid(tmpdir):
136136
assert path_to_invalid(tmpdir) == str(tmpdir) + ".invalid"
137137

138138

139+
@pytest.mark.skipif(sys.platform.startswith("win"), reason="requires POSIX directory perms")
140+
def test_atomic_writing_in_readonly_dir(tmp_path):
141+
# Setup: non-writable dir but a writable file inside
142+
nonw = tmp_path / "nonwritable"
143+
nonw.mkdir()
144+
f = nonw / "file.txt"
145+
f.write_text("original content")
146+
os.chmod(str(nonw), 0o500)
147+
os.chmod(str(f), 0o700)
148+
149+
# direct write fallback succeeds
150+
with atomic_writing(str(f)) as ff:
151+
ff.write("new content")
152+
assert f.read_text() == "new content"
153+
154+
# dir perms unchanged
155+
mode = stat.S_IMODE(os.stat(str(nonw)).st_mode)
156+
assert mode == 0o500
157+
158+
139159
@pytest.mark.skipif(os.name == "nt", reason="test fails on Windows")
140160
def test_file_manager_mixin(tmp_path):
141161
mixin = FileManagerMixin()

0 commit comments

Comments
 (0)