-
-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support PEP 723 with a toml load function (#811)
* feat: support PEP 723 with a toml load function Signed-off-by: Henry Schreiner <[email protected]> * refactor: nox.toml.load Signed-off-by: Henry Schreiner <[email protected]> * tests: fix coverage for module dir's Signed-off-by: Henry Schreiner <[email protected]> * refactor: nox.project.load_toml Signed-off-by: Henry Schreiner <[email protected]> * Update requirements-test.txt Co-authored-by: Claudio Jolowicz <[email protected]> * Update requirements-test.txt --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Claudio Jolowicz <[email protected]>
- Loading branch information
Showing
8 changed files
with
231 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
import re | ||
import sys | ||
from pathlib import Path | ||
from typing import TYPE_CHECKING | ||
|
||
if TYPE_CHECKING: | ||
from typing import Any | ||
|
||
if sys.version_info < (3, 11): | ||
import tomli as tomllib | ||
else: | ||
import tomllib | ||
|
||
|
||
__all__ = ["load_toml"] | ||
|
||
|
||
def __dir__() -> list[str]: | ||
return __all__ | ||
|
||
|
||
# Note: the implementation (including this regex) taken from PEP 723 | ||
# https://peps.python.org/pep-0723 | ||
|
||
REGEX = re.compile( | ||
r"(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$" | ||
) | ||
|
||
|
||
def load_toml(filename: os.PathLike[str] | str) -> dict[str, Any]: | ||
""" | ||
Load a toml file or a script with a PEP 723 script block. | ||
The file must have a ``.toml`` extension to be considered a toml file or a | ||
``.py`` extension / no extension to be considered a script. Other file | ||
extensions are not valid in this function. | ||
""" | ||
filepath = Path(filename) | ||
if filepath.suffix == ".toml": | ||
return _load_toml_file(filepath) | ||
if filepath.suffix in {".py", ""}: | ||
return _load_script_block(filepath) | ||
msg = f"Extension must be .py or .toml, got {filepath.suffix}" | ||
raise ValueError(msg) | ||
|
||
|
||
def _load_toml_file(filepath: Path) -> dict[str, Any]: | ||
with filepath.open("rb") as f: | ||
return tomllib.load(f) | ||
|
||
|
||
def _load_script_block(filepath: Path) -> dict[str, Any]: | ||
name = "script" | ||
script = filepath.read_text(encoding="utf-8") | ||
matches = list(filter(lambda m: m.group("type") == name, REGEX.finditer(script))) | ||
|
||
if not matches: | ||
raise ValueError(f"No {name} block found in {filepath}") | ||
if len(matches) > 1: | ||
raise ValueError(f"Multiple {name} blocks found in {filepath}") | ||
|
||
content = "".join( | ||
line[2:] if line.startswith("# ") else line[1:] | ||
for line in matches[0].group("content").splitlines(keepends=True) | ||
) | ||
return tomllib.loads(content) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
coverage[toml]>=5.3 | ||
coverage[toml]>=7.2 | ||
flask | ||
myst-parser | ||
pytest>=6.0 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import textwrap | ||
from pathlib import Path | ||
|
||
import pytest | ||
|
||
import nox | ||
|
||
|
||
def test_load_pyproject(tmp_path: Path) -> None: | ||
filepath = tmp_path / "example.toml" | ||
filepath.write_text( | ||
""" | ||
[project] | ||
name = "hi" | ||
version = "1.0" | ||
dependencies = ["numpy", "requests"] | ||
""" | ||
) | ||
|
||
toml = nox.project.load_toml(filepath) | ||
assert toml["project"]["dependencies"] == ["numpy", "requests"] | ||
|
||
|
||
@pytest.mark.parametrize("example", ["example.py", "example"]) | ||
def test_load_script_block(tmp_path: Path, example: str) -> None: | ||
filepath = tmp_path / example | ||
filepath.write_text( | ||
textwrap.dedent( | ||
"""\ | ||
#!/usr/bin/env pipx run | ||
# /// script | ||
# requires-python = ">=3.11" | ||
# dependencies = [ | ||
# "requests<3", | ||
# "rich", | ||
# ] | ||
# /// | ||
import requests | ||
from rich.pretty import pprint | ||
resp = requests.get("https://peps.python.org/api/peps.json") | ||
data = resp.json() | ||
pprint([(k, v["title"]) for k, v in data.items()][:10]) | ||
""" | ||
) | ||
) | ||
|
||
toml = nox.project.load_toml(filepath) | ||
assert toml["dependencies"] == ["requests<3", "rich"] | ||
|
||
|
||
def test_load_no_script_block(tmp_path: Path) -> None: | ||
filepath = tmp_path / "example.py" | ||
filepath.write_text( | ||
textwrap.dedent( | ||
"""\ | ||
#!/usr/bin/python | ||
import requests | ||
from rich.pretty import pprint | ||
resp = requests.get("https://peps.python.org/api/peps.json") | ||
data = resp.json() | ||
pprint([(k, v["title"]) for k, v in data.items()][:10]) | ||
""" | ||
) | ||
) | ||
|
||
with pytest.raises(ValueError, match="No script block found"): | ||
nox.project.load_toml(filepath) | ||
|
||
|
||
def test_load_multiple_script_block(tmp_path: Path) -> None: | ||
filepath = tmp_path / "example.py" | ||
filepath.write_text( | ||
textwrap.dedent( | ||
"""\ | ||
# /// script | ||
# dependencies = [ | ||
# "requests<3", | ||
# "rich", | ||
# ] | ||
# /// | ||
# /// script | ||
# requires-python = ">=3.11" | ||
# /// | ||
import requests | ||
from rich.pretty import pprint | ||
resp = requests.get("https://peps.python.org/api/peps.json") | ||
data = resp.json() | ||
pprint([(k, v["title"]) for k, v in data.items()][:10]) | ||
""" | ||
) | ||
) | ||
|
||
with pytest.raises(ValueError, match="Multiple script blocks found"): | ||
nox.project.load_toml(filepath) | ||
|
||
|
||
def test_load_non_recongnised_extension(): | ||
msg = "Extension must be .py or .toml, got .txt" | ||
with pytest.raises(ValueError, match=msg): | ||
nox.project.load_toml("some.txt") |