Repository: wntrblm/nox (language: python)

Issue:
Wrong starting directory when noxfile.py is a symlink
I have a project where I want the noxfille to be a symlink to a different shared file.  When I do this, it throws off the paths b/c nox follows the symlink and starts in that directory instead of starting in the directory where the symlink is.

```
 ❯ ls -l noxfile.py 
lrwxrwxrwx 1 rsyring rsyring 21 May 15 23:09 noxfile.py -> ./template/noxfile.py

 ❯ nox -s cwd
nox > Running session cwd
nox > Creating virtual environment (uv) using python3 in .nox/cwd
cwd is: /home/rsyring/projects/copier-py-package/template
```

Was this an intentional design decision?  If not, how about starting where the symlink is instead (i.e. don't follow it)?

Thanks for your work on this project and consideration.  



Discussion / hints:
Thanks, PR welcome. Take a look in `load_nox_module` in tasks.py.
That happens here: https://github.com/wntrblm/nox/blob/a6e1a4efaeea3d67dd2c47b80b9b8836a4aa579a/nox/tasks.py#L86

The one below is fine (takes the global path of the dir), maybe it should just use that and reassemble the name.

Fix patch:
diff --git a/nox/tasks.py b/nox/tasks.py
index e2576416..62bee9b5 100644
--- a/nox/tasks.py
+++ b/nox/tasks.py
@@ -83,13 +83,15 @@ def load_nox_module(global_config: Namespace) -> types.ModuleType | int:
     # Be sure to expand variables
     global_config_noxfile = os.path.expandvars(global_config.noxfile)
 
+    # Make sure we only expand the parent dir just in case the noxfile is a symlink
+    noxfile_parent_dir = os.path.realpath(os.path.dirname(global_config_noxfile))
+
     # Save the absolute path to the Noxfile.
     # This will inoculate it if Nox changes paths because of an implicit
     # or explicit chdir (like the one below).
-    global_config.noxfile = os.path.realpath(global_config_noxfile)
-
-    # Make sure we only expand the parent dir just in case the noxfile is a symlink
-    noxfile_parent_dir = os.path.realpath(os.path.dirname(global_config.noxfile))
+    global_config.noxfile = os.path.join(
+        noxfile_parent_dir, os.path.basename(global_config_noxfile)
+    )
 
     try:
         # Check ``nox.needs_version`` by parsing the AST.


Test patch:
diff --git a/tests/resources/orig_dir/noxfile.py b/tests/resources/orig_dir/noxfile.py
new file mode 100644
index 00000000..f6eb8ab6
--- /dev/null
+++ b/tests/resources/orig_dir/noxfile.py
@@ -0,0 +1,17 @@
+from pathlib import Path
+
+import nox
+
+FILE = Path(__file__).resolve()
+
+
+@nox.session(venv_backend="none", default=False)
+def orig(session: nox.Session) -> None:
+    assert Path("orig_file.txt").exists()
+
+
+@nox.session(venv_backend="none", default=False)
+def sym(session: nox.Session) -> None:
+    assert Path("sym_file.txt").exists()
+
+    assert FILE.parent.joinpath("orig_file.txt").exists()
diff --git a/tests/resources/orig_dir/orig_file.txt b/tests/resources/orig_dir/orig_file.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/resources/sym_dir/noxfile.py b/tests/resources/sym_dir/noxfile.py
new file mode 120000
index 00000000..67bd5e2c
--- /dev/null
+++ b/tests/resources/sym_dir/noxfile.py
@@ -0,0 +1,1 @@
+../orig_dir/noxfile.py
\ No newline at end of file
diff --git a/tests/resources/sym_dir/sym_file.txt b/tests/resources/sym_dir/sym_file.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/test_main.py b/tests/test_main.py
index 0cb737a8..72bbe755 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -16,6 +16,7 @@
 
 import contextlib
 import os
+import subprocess
 import sys
 from importlib import metadata
 from pathlib import Path
@@ -928,3 +929,25 @@ def test_noxfile_options_cant_be_set():
 def test_noxfile_options_cant_be_set_long():
     with pytest.raises(AttributeError, match="i_am_clearly_not_an_option"):
         nox.options.i_am_clearly_not_an_option = True
+
+
+def test_symlink_orig(monkeypatch):
+    monkeypatch.chdir(Path(RESOURCES) / "orig_dir")
+    subprocess.run([sys.executable, "-m", "nox", "-s", "orig"], check=True)
+
+
+def test_symlink_orig_not(monkeypatch):
+    monkeypatch.chdir(Path(RESOURCES) / "orig_dir")
+    res = subprocess.run([sys.executable, "-m", "nox", "-s", "sym"], check=False)
+    assert res.returncode == 1
+
+
+def test_symlink_sym(monkeypatch):
+    monkeypatch.chdir(Path(RESOURCES) / "sym_dir")
+    subprocess.run([sys.executable, "-m", "nox", "-s", "sym"], check=True)
+
+
+def test_symlink_sym_not(monkeypatch):
+    monkeypatch.chdir(Path(RESOURCES) / "sym_dir")
+    res = subprocess.run([sys.executable, "-m", "nox", "-s", "orig"], check=False)
+    assert res.returncode == 1