Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow notebooks in other locations #162

Merged
merged 6 commits into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
Please read the [Development - Contributing](https://dj-notebook.readthedocs.io/en/latest/contributing/) guidelines in the documentation site.

5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pip install dj_notebook

First, find your project's `manage.py` file and open it. Copy whatever is being set to `DJANGO_SETTINGS_MODULE` into your clipboard.

Create an ipython notebook in the same directory as `manage.py`. In VSCode,
Create an ipython notebook in the same directory as `manage.py`, or another directory of your choosing. In VSCode,
simply add a new `.ipynb` file. If using Jupyter Lab, use the `File -> New ->
Notebook` menu option.

Expand All @@ -50,6 +50,9 @@ from dj_notebook import activate

plus = activate()

# If you have created your notebook in a different directory, instead do:
# plus = activate(search_dir="/path/to/your/project")

# If that throws an error, try one of the following:

# DJANGO_SETTINGS_MODULE_VALUE aka "book_store.settings"
Expand Down
9 changes: 7 additions & 2 deletions src/dj_notebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@


def activate(
settings: str = None, quiet_load: bool = True, *, dotenv_file: StrPath | None = None
settings: str | None = None,
quiet_load: bool = True,
*,
dotenv_file: StrPath | None = None,
search_dir: StrPath | None = None,
) -> Plus:
with Status(
"Loading dj-notebook...\n Use Plus.print() to see what's been loaded.",
Expand All @@ -31,7 +35,8 @@ def activate(
os.environ["DJANGO_SETTINGS_MODULE"] = settings
else:
source, discovered_settings = find_django_settings_module(
dotenv_file=dotenv_file
dotenv_file=dotenv_file,
search_dir=search_dir,
)
if discovered_settings:
if not quiet_load:
Expand Down
41 changes: 25 additions & 16 deletions src/dj_notebook/config_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,28 +55,34 @@ def is_root(path: Path) -> bool:
# dotenv_file should be rewritten as Optional[StrPath] = None and the return type should be annotated as
# Tuple[str, Optional[str]]
def find_django_settings_module(
*, dotenv_file: StrPath | None = None
*,
dotenv_file: StrPath | None = None,
search_dir: StrPath | None = None,
) -> Tuple[str, str | None]:
"""
Find the name of the first settings module from the environment or the closest `manage.py` file.
Returns: a tuple(source, module name) telling the caller where the module was found and the name of the module.

The optional, keyword-only argument `dotenv_file` will be explicitly loaded prior to searching the environment,
if supplied.
Optional keyword-only arguments:
dotenv_file: Absolute or relative path to .env file, loaded prior to searching the environment.
search_dir: Absolute or relative path to the directory to start searching for a `manage.py` file, used if
`dotenv_file` is `None`.
If both `dotenv_file` and `search_dir` are `None`, the environment variable DJANGO_SETTINGS_MODULE is checked,
and the current working directory (and its parents and immediate subdirectories) is searched for a `manage.py` file.
"""
settings_module = None
# First see if this has either already been set in the environment or put in a .env file that python-dotenv will
# treat that way
settings_module = None
if not dotenv_file:
source = "environment"
settings_module = os.environ.get("DJANGO_SETTINGS_MODULE", None)
if not settings_module:
if dotenv_file:
# load with override=True if the caller has specified a dotenv file explicitly
source = "dotenv"
load_dotenv(dotenv_file, override=bool(dotenv_file))
settings_module = os.environ.get("DJANGO_SETTINGS_MODULE", None)
settings_module = os.environ.get("DJANGO_SETTINGS_MODULE", None)
elif not search_dir:
source = "environment"
settings_module = os.environ.get("DJANGO_SETTINGS_MODULE", None)
# If we get nothing from the environment, look for a `manage.py` script containing a call that sets a default in the
# current working directory or in any parent. This should accommodate the common pattern of
# search directory, the current working directory, or any parent. This should accommodate the common pattern of
# - app1
# - app2
# - project
Expand All @@ -85,9 +91,9 @@ def find_django_settings_module(
# - notebooks
# --> analysis_notebook.ipynb
# - manage.py
search_dir = Path.cwd().resolve()
current_search_dir = Path(search_dir or Path.cwd()).resolve()
while settings_module is None:
manage_py = search_dir / "manage.py"
manage_py = current_search_dir / "manage.py"
if manage_py.is_file():
for call in setdefault_calls(manage_py):
if (
Expand All @@ -96,20 +102,23 @@ def find_django_settings_module(
):
settings_module = call.args[1].value
source = f"{manage_py.resolve().absolute()}"
elif is_root(search_dir):
elif is_root(current_search_dir):
break
else:
search_dir = search_dir.parent.resolve()
current_search_dir = current_search_dir.parent.resolve()
if not settings_module:
# Finally, go one level down into children of the current working directory to see if a `manage.py` with a default
# Finally, go one level down into children of the search directory to see if a `manage.py` with a default
# for `DJANGO_SETTINGS_MODULE` can be found there. This accommodates the common pattern of
# - analysis.ipynb
# - src
# --> manage.py
# --> project
# ----> settings.py
# ...
for p in [Path(subdir) for subdir in os.scandir(Path.cwd())]:
for p in [
Path(subdir)
for subdir in os.scandir(Path(search_dir or Path.cwd()).resolve())
]:
manage_py = p / "manage.py"
if manage_py.is_file():
for call in setdefault_calls(manage_py):
Expand Down
29 changes: 29 additions & 0 deletions tests/test_config_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,32 @@ def test_find_django_settings_module_os_environment():
assert source == "environment"
assert os.environ["DJANGO_SETTINGS_MODULE"] == found
assert found == "something.else"


def test_find_django_settings_module_remote_path():
script_dir_path = Path(os.path.dirname(os.path.realpath(__file__)))
django_project_path = script_dir_path / "django_test_project"
old_cwd = os.getcwd()
# Change to a directory that is not the django project or adjacent.
os.chdir(script_dir_path / "..")
with EnvironmentGuard():
source, found = find_django_settings_module(search_dir=django_project_path)
assert source == str(django_project_path / "manage.py")
assert found == "book_store.settings"
# Change to the parent directory in order to search children
source, found = None, None
with EnvironmentGuard():
source, found = find_django_settings_module(
search_dir=django_project_path / ".."
)
assert source == str(django_project_path / "manage.py")
assert found == "book_store.settings"
# Change to a child directory in order to search parents
source, found = None, None
with EnvironmentGuard():
source, found = find_django_settings_module(
search_dir=django_project_path / "book_store"
)
assert source == str(django_project_path / "manage.py")
assert found == "book_store.settings"
os.chdir(old_cwd)