Skip to content

Commit c95d6d3

Browse files
authored
๐Ÿšธ Support inferring notebook path when executed through nbconvert (#288)
* ๐Ÿšธ Support inferring notebook path when executed through nbconvert * โœ… Add a test * โž• Add nbconvert
1 parent 13a5392 commit c95d6d3

File tree

5 files changed

+156
-2
lines changed

5 files changed

+156
-2
lines changed

โ€Ždocs/guide/received.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"cell_type": "markdown",
3030
"metadata": {},
3131
"source": [
32-
"The header now makes package verion mismatches evident. "
32+
"The header now makes package version mismatches evident. "
3333
]
3434
},
3535
{

โ€Žnbproject/dev/_jupyter_communicate.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import sys
33
from itertools import chain
4-
from pathlib import PurePath
4+
from pathlib import Path, PurePath
55
from urllib import request
66

77
import orjson
@@ -67,6 +67,113 @@ def running_servers():
6767
return servers_nbapp, servers_juserv
6868

6969

70+
def find_nb_path_via_parent_process():
71+
"""Tries to find the notebook path by inspecting the parent process's command line.
72+
73+
Requires the 'psutil' library. Heuristic and potentially fragile.
74+
"""
75+
import psutil
76+
77+
try:
78+
current_process = psutil.Process(os.getpid())
79+
parent_process = current_process.parent()
80+
81+
if parent_process is None:
82+
logger.warning("psutil: Could not get parent process.")
83+
return None
84+
85+
# Get parent command line arguments
86+
cmdline = parent_process.cmdline()
87+
if not cmdline:
88+
logger.warning(
89+
f"psutil: Parent process ({parent_process.pid}) has empty cmdline."
90+
)
91+
# Maybe check grandparent? This gets complicated quickly.
92+
return None
93+
94+
logger.info(f"psutil: Parent cmdline: {cmdline}")
95+
96+
# Heuristic parsing: Look for 'nbconvert' and '.ipynb'
97+
# This is fragile and depends on how nbconvert was invoked.
98+
is_nbconvert_call = False
99+
potential_path = None
100+
101+
for i, arg in enumerate(cmdline):
102+
# Check if 'nbconvert' command is present
103+
if "nbconvert" in arg.lower():
104+
# Check if it's the main command (e.g. /path/to/jupyter-nbconvert)
105+
# or a subcommand (e.g. ['jupyter', 'nbconvert', ...])
106+
# or a module call (e.g. ['python', '-m', 'nbconvert', ...])
107+
base_arg = os.path.basename(arg).lower() # noqa: PTH119
108+
if (
109+
"jupyter-nbconvert" in base_arg
110+
or arg == "nbconvert"
111+
or (
112+
cmdline[i - 1].endswith("python")
113+
and arg == "-m"
114+
and cmdline[i + 1] == "nbconvert"
115+
)
116+
):
117+
is_nbconvert_call = True
118+
119+
# Find the argument ending in .ipynb AFTER 'nbconvert' is likely found
120+
# Or just find the last argument ending in .ipynb as a guess
121+
if arg.endswith(".ipynb"):
122+
potential_path = arg # Store the last one found
123+
124+
if is_nbconvert_call and potential_path:
125+
# We found something that looks like an nbconvert call and an ipynb file
126+
# The path might be relative to the parent process's CWD.
127+
# Try to resolve it. Parent CWD might not be notebook dir if called like
128+
# jupyter nbconvert --execute /abs/path/to/notebook.ipynb
129+
try:
130+
# Get parent's CWD
131+
parent_cwd = parent_process.cwd()
132+
resolved_path = Path(parent_cwd) / Path(potential_path)
133+
if resolved_path.is_file():
134+
logger.info(f"psutil: Found potential path: {resolved_path}")
135+
return resolved_path.resolve() # Return absolute path
136+
else:
137+
# Maybe the path was already absolute?
138+
abs_path = Path(potential_path)
139+
if abs_path.is_absolute() and abs_path.is_file():
140+
logger.info(
141+
f"psutil: Found potential absolute path: {abs_path}"
142+
)
143+
return abs_path.resolve()
144+
else:
145+
logger.warning(
146+
f"psutil: Potential path '{potential_path}' not found relative to parent CWD '{parent_cwd}' or as absolute path."
147+
)
148+
return None
149+
150+
except psutil.AccessDenied:
151+
logger.warning("psutil: Access denied when getting parent CWD.")
152+
# Fallback: assume path might be relative to kernel's CWD (less likely)
153+
maybe_path = Path(potential_path)
154+
if maybe_path.is_file():
155+
return maybe_path.resolve()
156+
return None # Give up trying to resolve relative path
157+
except Exception as e:
158+
logger.warning(f"psutil: Error resolving path '{potential_path}': {e}")
159+
return None
160+
161+
logger.warning(
162+
"psutil: Could not reliably identify notebook path from parent cmdline."
163+
)
164+
return None
165+
166+
except ImportError:
167+
logger.warning("psutil library not found. Cannot inspect parent process.")
168+
return None
169+
except psutil.Error as e:
170+
logger.warning(f"psutil error: {e}")
171+
return None
172+
except Exception as e:
173+
logger.warning(f"Unexpected error during psutil check: {e}")
174+
return None
175+
176+
70177
def notebook_path(return_env=False):
71178
"""Return the path to the current notebook.
72179
@@ -157,6 +264,16 @@ def notebook_path(return_env=False):
157264
nb_path = PurePath(os.environ["JPY_SESSION_NAME"])
158265
return (nb_path, "lab" if env is None else env) if return_env else nb_path
159266

267+
# try inspecting parent process using psutil, needed if notebook is run via nbconvert
268+
nb_path_psutil = find_nb_path_via_parent_process()
269+
if nb_path_psutil is not None:
270+
logger.info("Detected path via psutil parent process inspection.")
271+
return (
272+
(nb_path_psutil, "nbconvert" if env is None else env)
273+
if return_env
274+
else nb_path_psutil
275+
)
276+
160277
# no running servers
161278
if servers_nbapp == [] and servers_juserv == []:
162279
logger.warning("Can not find any servers running.")

โ€Žpyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"pyyaml",
2020
"packaging",
2121
"orjson",
22+
"psutil",
2223
"importlib-metadata",
2324
"stdlib_list; python_version < '3.10'",
2425
"lamin_utils>=0.13.2",
@@ -38,6 +39,7 @@ dev = [
3839
"nbproject_test >= 0.4.5",
3940
"laminci",
4041
"ipylab",
42+
"nbconvert",
4143
]
4244

4345
[project.scripts]

โ€Žtests/for-nbconvert.ipynb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"from nbproject.dev._jupyter_communicate import notebook_path\n",
10+
"\n",
11+
"assert notebook_path() is not None, \"Cannot infer notebook path.\""
12+
]
13+
}
14+
],
15+
"metadata": {
16+
"language_info": {
17+
"name": "python"
18+
}
19+
},
20+
"nbformat": 4,
21+
"nbformat_minor": 2
22+
}

โ€Žtests/test_nbconvert.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import subprocess
2+
from pathlib import Path
3+
4+
5+
def test_running_via_nbconvert():
6+
result = subprocess.run(
7+
"jupyter nbconvert --to notebook --execute ./tests/for-nbconvert.ipynb",
8+
shell=True,
9+
capture_output=True,
10+
)
11+
print(result.stdout.decode())
12+
print(result.stderr.decode())
13+
assert result.returncode == 0

0 commit comments

Comments
ย (0)