Skip to content

Commit f66e4d0

Browse files
Add functions for finding the .jupyter directory or the workspace directory (#1376)
* Add find_dotjupyter_dir function to locate .jupyter directory This commit introduces the find_dotjupyter_dir function, which traverses up the directory tree from a given path to find the nearest .jupyter directory. Additionally, it includes unit tests to verify the functionality of this new feature, ensuring it correctly identifies the .jupyter directory when present and returns None when absent. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixes tests. * Make hidden files visible on server. * Fix optional typing of dotjupyter. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add support to stop at the root_dir. * Add handling of permission errors. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add typing for server_app. * Wire up .jupyter dir to persona manager and base persona. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Refactor directory utilities: extract and generalize .jupyter directory logic The changes refactor the .jupyter directory finding functionality by: - Removing specific dotjupyter.py module and its tests - Adding new generalized directories.py utilities for finding dot directories - Moving .jupyter directory methods from middle of BasePersona to end for better organization - Adding workspace directory support alongside .jupyter directory functionality - Updating PersonaManager to use the new generalized directory utilities This consolidates directory traversal logic and makes it more reusable for different directory types. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix typing. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Precommit fixes. * Update logic in find_dot_dir. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 02a823d commit f66e4d0

File tree

5 files changed

+404
-1
lines changed

5 files changed

+404
-1
lines changed

packages/jupyter-ai/jupyter_ai/extension.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from jupyter_ai_magics.utils import get_em_providers, get_lm_providers
1111
from jupyter_events import EventLogger
1212
from jupyter_server.extension.application import ExtensionApp
13+
from jupyter_server.serverapp import ServerApp
1314
from jupyter_server_fileid.manager import ( # type: ignore[import-untyped]
1415
BaseFileIdManager,
1516
)
@@ -18,6 +19,7 @@
1819
from pycrdt import ArrayEvent
1920
from tornado.web import StaticFileHandler
2021
from traitlets import Integer, List, Type, Unicode
22+
from traitlets.config import Config
2123

2224
from .completions.handlers import DefaultInlineCompletionHandler
2325
from .config_manager import ConfigManager
@@ -434,3 +436,10 @@ def _init_persona_manager(
434436
self.log.exception(e)
435437
finally:
436438
return persona_manager
439+
440+
def _link_jupyter_server_extension(self, server_app: ServerApp):
441+
"""Setup custom config needed by this extension."""
442+
c = Config()
443+
c.ContentsManager.allow_hidden = True
444+
server_app.update_config(c)
445+
super()._link_jupyter_server_extension(server_app)

packages/jupyter-ai/jupyter_ai/personas/base_persona.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,18 @@ def get_chat_dir(self) -> str:
329329
"""
330330
return self.parent.get_chat_dir()
331331

332+
def get_dotjupyter_dir(self) -> Optional[str]:
333+
"""
334+
Returns the path to the .jupyter directory for the current chat.
335+
"""
336+
return self.parent.get_dotjupyter_dir()
337+
338+
def get_workspace_dir(self) -> str:
339+
"""
340+
Returns the path to the workspace directory for the current chat.
341+
"""
342+
return self.parent.get_workspace_dir()
343+
332344

333345
class GenerationInterrupted(asyncio.CancelledError):
334346
"""Exception raised when streaming is cancelled by the user"""
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from pathlib import Path
2+
from typing import Optional
3+
4+
5+
def find_dot_dir(
6+
dir: str, dot_dir: str, root_dir: Optional[str] = None
7+
) -> Optional[str]:
8+
"""
9+
Find the nearest dot directory by traversing up from the given directory.
10+
11+
Args:
12+
dir (str): The starting directory path
13+
dot_dir (str): The dot directory name to search for (e.g., '.jupyter', '.git')
14+
root_dir (Optional[str]): The root directory to stop searching at.
15+
If None, searches to filesystem root.
16+
17+
Returns:
18+
str: The absolute path to the dot directory if found, None otherwise
19+
20+
Raises:
21+
ValueError: If dir is not a directory
22+
"""
23+
current_path = Path(dir).resolve()
24+
25+
# Validate that the starting path is a directory
26+
if not current_path.is_dir():
27+
raise ValueError(f"'{dir}' is not a directory")
28+
29+
# Convert root_dir to resolved path if provided
30+
root_path = Path(root_dir).resolve() if root_dir is not None else None
31+
32+
while current_path != current_path.parent: # Stop at filesystem root
33+
target_dir = current_path / dot_dir
34+
try:
35+
if target_dir.is_dir():
36+
return str(target_dir)
37+
except PermissionError:
38+
# Stop searching if we don't have permission to access this directory
39+
break
40+
41+
# Stop if we've reached the specified root directory
42+
if root_path is not None and current_path == root_path:
43+
break
44+
45+
current_path = current_path.parent
46+
47+
return None
48+
49+
50+
def find_workspace_dir(dir: str, root_dir: Optional[str] = None) -> str:
51+
"""
52+
Find the workspace directory using the following algorithm:
53+
54+
1. First finds the nearest .jupyter directory
55+
2. If the .jupyter directory has a parent that is the root_dir,
56+
go to step 3, otherwise return the .jupyter directory
57+
3. Try to find a .git directory, if found return that
58+
4. Simply return the directory the chat file is in.
59+
60+
Args:
61+
dir (str): The starting directory path
62+
root_dir (Optional[str]): The root directory to stop searching at. If None, searches to filesystem root.
63+
64+
Returns:
65+
str: The absolute path to the workspace directory
66+
67+
Raises:
68+
ValueError: If dir is not a directory
69+
"""
70+
# Validate that the starting path is a directory
71+
dir_path = Path(dir).resolve()
72+
if not dir_path.is_dir():
73+
raise ValueError(f"'{dir}' is not a directory")
74+
75+
# Step 1: Find nearest .jupyter directory
76+
jupyter_dir = find_dot_dir(dir, ".jupyter", root_dir)
77+
78+
if jupyter_dir is not None:
79+
jupyter_path = Path(jupyter_dir)
80+
done = True
81+
# Step 2: Check if .jupyter directory's parent is the root_dir, if so, go to step 3
82+
if root_dir is not None:
83+
root_path = Path(root_dir).resolve()
84+
if jupyter_path.parent == root_path:
85+
done = False
86+
if done:
87+
return str(jupyter_path.parent)
88+
89+
# Step 3: Try to find .git directory (if no .jupyter found)
90+
git_dir = find_dot_dir(dir, ".git", root_dir)
91+
if git_dir is not None:
92+
return str(Path(git_dir).parent)
93+
94+
# Step 4: Return chat file directory
95+
return str(dir_path)

packages/jupyter-ai/jupyter_ai/personas/persona_manager.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from ..config_manager import ConfigManager
1515
from .base_persona import BasePersona
16+
from .directories import find_dot_dir, find_workspace_dir
1617

1718
if TYPE_CHECKING:
1819
from asyncio import AbstractEventLoop
@@ -258,5 +259,17 @@ def get_chat_dir(self) -> str:
258259
Returns the absolute path of the parent directory of the chat file
259260
assigned to this `PersonaManager`.
260261
"""
261-
abspath = self.get_chat_path(absolute=True)
262+
abspath = self.get_chat_path()
262263
return os.path.dirname(abspath)
264+
265+
def get_dotjupyter_dir(self) -> str | None:
266+
"""
267+
Returns the path to the .jupyter directory for the current chat.
268+
"""
269+
return find_dot_dir(self.get_chat_dir(), ".jupyter", root_dir=self.root_dir)
270+
271+
def get_workspace_dir(self) -> str:
272+
"""
273+
Returns the path to the workspace directory for the current chat.
274+
"""
275+
return find_workspace_dir(self.get_chat_dir(), root_dir=self.root_dir)

0 commit comments

Comments
 (0)