Skip to content

Commit c6541cd

Browse files
authored
fix: support scripts with custom names (#1007)
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
1 parent e7a288d commit c6541cd

File tree

7 files changed

+94
-11
lines changed

7 files changed

+94
-11
lines changed

nox/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from __future__ import annotations # pragma: no cover
2323

24-
from nox._cli import main # pragma: no cover
24+
from nox._cli import nox_main as main # pragma: no cover
2525

2626
__all__ = ["main"] # pragma: no cover
2727

nox/_cli.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@
3131
import nox.command
3232
import nox.virtualenv
3333
from nox import _options, tasks, workflow
34+
from nox._options import DefaultStr
3435
from nox._version import get_nox_version
3536
from nox.logger import logger, setup_logging
3637
from nox.project import load_toml
3738

3839
if TYPE_CHECKING:
3940
from collections.abc import Generator
4041

41-
__all__ = ["execute_workflow", "main"]
42+
__all__ = ["execute_workflow", "main", "nox_main"]
4243

4344

4445
def __dir__() -> list[str]:
@@ -136,7 +137,19 @@ def check_url_dependency(dep_url: str, dist: importlib.metadata.Distribution) ->
136137
return dep_purl.netloc == origin_purl.netloc and dep_purl.path == origin_purl.path
137138

138139

140+
def get_main_filename() -> str | None:
141+
main_module = sys.modules.get("__main__")
142+
if (
143+
main_module
144+
and (fname := getattr(main_module, "__file__", ""))
145+
and os.path.exists(main_filename := os.path.abspath(fname))
146+
):
147+
return main_filename
148+
return None
149+
150+
139151
def run_script_mode(
152+
noxfile: str,
140153
envdir: Path,
141154
*,
142155
reuse: bool,
@@ -163,11 +176,12 @@ def run_script_mode(
163176
subprocess.run([*cmd, *dependencies], env=env, check=True)
164177
nox_cmd = shutil.which("nox", path=env["PATH"])
165178
assert nox_cmd is not None, "Nox must be discoverable when installed"
179+
args = [nox_cmd, "-f", noxfile, *sys.argv[1:]]
166180
# The os.exec functions don't work properly on Windows
167181
if sys.platform.startswith("win"):
168182
raise SystemExit(
169183
subprocess.run(
170-
[nox_cmd, *sys.argv[1:]],
184+
args,
171185
env=env,
172186
stdout=None,
173187
stderr=None,
@@ -176,10 +190,18 @@ def run_script_mode(
176190
check=False,
177191
).returncode
178192
)
179-
os.execle(nox_cmd, nox_cmd, *sys.argv[1:], env) # pragma: nocover # noqa: S606
193+
os.execle(nox_cmd, *args, env) # pragma: nocover # noqa: S606
180194

181195

182196
def main() -> None:
197+
_main(main_ep=False)
198+
199+
200+
def nox_main() -> None:
201+
_main(main_ep=True)
202+
203+
204+
def _main(*, main_ep: bool) -> None:
183205
args = _options.options.parse_args()
184206

185207
if args.help:
@@ -198,7 +220,12 @@ def main() -> None:
198220
msg = f"Invalid NOX_SCRIPT_MODE: {nox_script_mode!r}, must be one of 'none', 'reuse', or 'fresh'"
199221
raise SystemExit(msg)
200222
if nox_script_mode != "none":
201-
toml_config = load_toml(os.path.expandvars(args.noxfile), missing_ok=True)
223+
noxfile = (
224+
args.noxfile
225+
if main_ep or not isinstance(args.noxfile, DefaultStr)
226+
else (get_main_filename() or args.noxfile)
227+
)
228+
toml_config = load_toml(os.path.expandvars(noxfile), missing_ok=True)
202229
dependencies = toml_config.get("dependencies")
203230
if dependencies is not None:
204231
valid_env = check_dependencies(dependencies)
@@ -235,6 +262,7 @@ def main() -> None:
235262

236263
envdir = Path(args.envdir or ".nox")
237264
run_script_mode(
265+
noxfile,
238266
envdir,
239267
reuse=nox_script_mode == "reuse",
240268
dependencies=dependencies,

nox/_options.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def __dir__() -> list[str]:
4242
return __all__
4343

4444

45+
# User-specified arguments will be a regular string
46+
class DefaultStr(str):
47+
__slots__ = ()
48+
49+
4550
ReuseVenvType = Literal["no", "yes", "never", "always"]
4651

4752
options = _option_set.OptionSet(
@@ -515,7 +520,7 @@ def _tag_completer(
515520
"-f",
516521
"--noxfile",
517522
group=options.groups["general"],
518-
default="noxfile.py",
523+
default=DefaultStr("noxfile.py"),
519524
help="Location of the Python file containing Nox sessions.",
520525
),
521526
_option_set.Option(

nox/tasks.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def _load_and_exec_nox_module(global_config: Namespace) -> types.ModuleType:
6868
types.ModuleType: The initialised Nox module.
6969
"""
7070
spec = importlib.util.spec_from_file_location(
71-
"user_nox_module", global_config.noxfile
71+
"user_nox_module", str(global_config.noxfile)
7272
)
7373
assert spec is not None # If None, fatal importlib error, would crash anyway
7474

@@ -107,8 +107,9 @@ def load_nox_module(global_config: Namespace) -> types.ModuleType | int:
107107
# Save the absolute path to the Noxfile.
108108
# This will inoculate it if Nox changes paths because of an implicit
109109
# or explicit chdir (like the one below).
110-
global_config.noxfile = os.path.join(
111-
noxfile_parent_dir, os.path.basename(global_config_noxfile)
110+
# Keeps the class of the original string
111+
global_config.noxfile = global_config.noxfile.__class__(
112+
os.path.join(noxfile_parent_dir, os.path.basename(global_config_noxfile))
112113
)
113114

114115
try:
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env python
2+
3+
# /// script
4+
# dependencies = ["nox", "cowsay"]
5+
# ///
6+
7+
8+
import nox
9+
10+
11+
@nox.session
12+
def exec_example(session: nox.Session) -> None:
13+
# Importing inside the function so that if the test fails,
14+
# it shows a better failure than immediately failing to import
15+
import cowsay # noqa: PLC0415
16+
17+
print(cowsay.cow("another_world"))
18+
19+
20+
if __name__ == "__main__":
21+
nox.main()

tests/test__cli.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def test_invalid_backend_envvar(
8787
) -> None:
8888
monkeypatch.setenv("NOX_SCRIPT_VENV_BACKEND", "invalid")
8989
monkeypatch.setattr(sys, "argv", ["nox"])
90+
# This will return pytest's filename instead, so patching it to None
91+
monkeypatch.setattr(nox._cli, "get_main_filename", lambda: None)
9092
monkeypatch.chdir(tmp_path)
9193
tmp_path.joinpath("noxfile.py").write_text(
9294
"# /// script\n# dependencies=['nox', 'invalid']\n# ///",
@@ -101,6 +103,8 @@ def test_invalid_backend_inline(
101103
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
102104
) -> None:
103105
monkeypatch.setattr(sys, "argv", ["nox"])
106+
# This will return pytest's filename instead, so patching it to None
107+
monkeypatch.setattr(nox._cli, "get_main_filename", lambda: None)
104108
monkeypatch.chdir(tmp_path)
105109
tmp_path.joinpath("noxfile.py").write_text(
106110
"# /// script\n# dependencies=['nox', 'invalid']\n# tool.nox.script-venv-backend = 'invalid'\n# ///",

tests/test_main.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import pytest
2828

2929
import nox
30+
import nox._cli
3031
import nox._options
3132
import nox.registry
3233
import nox.sessions
@@ -44,14 +45,17 @@
4445
os.environ.pop("NOXSESSION", None)
4546

4647

47-
def test_main_no_args(monkeypatch: pytest.MonkeyPatch) -> None:
48+
@pytest.mark.parametrize(
49+
"main", [nox.main, nox._cli.nox_main], ids=["main", "nox_main"]
50+
)
51+
def test_main_no_args(monkeypatch: pytest.MonkeyPatch, main: Any) -> None:
4852
monkeypatch.setattr(sys, "argv", [sys.executable])
4953
with mock.patch("nox.workflow.execute") as execute:
5054
execute.return_value = 0
5155

5256
# Call the function.
5357
with mock.patch.object(sys, "exit") as exit:
54-
nox.main()
58+
main()
5559
exit.assert_called_once_with(0)
5660
assert execute.called
5761

@@ -1098,6 +1102,26 @@ def test_noxfile_no_script_mode(monkeypatch: pytest.MonkeyPatch) -> None:
10981102
assert "No module named 'cowsay'" in job.stderr
10991103

11001104

1105+
def test_noxfile_script_mode_exec(monkeypatch: pytest.MonkeyPatch) -> None:
1106+
monkeypatch.delenv("NOX_SCRIPT_MODE", raising=False)
1107+
job = subprocess.run(
1108+
[
1109+
sys.executable,
1110+
Path(RESOURCES) / "noxfile_script_mode_exec.py",
1111+
"-s",
1112+
"exec_example",
1113+
],
1114+
check=False,
1115+
capture_output=True,
1116+
text=True,
1117+
encoding="utf-8",
1118+
)
1119+
print(job.stdout)
1120+
print(job.stderr)
1121+
assert job.returncode == 0
1122+
assert "another_world" in job.stdout
1123+
1124+
11011125
def test_noxfile_script_mode_url_req() -> None:
11021126
job = subprocess.run(
11031127
[

0 commit comments

Comments
 (0)