|
1 | | -"""Utilities for reading Python version from pyproject.toml.""" |
| 1 | +"""Utilities for reading values from pyproject.toml.""" |
2 | 2 |
|
3 | | -_TOML2JSON = Label("//tools/private/toml2json:toml2json.py") |
| 3 | +load("@toml.bzl", "toml") |
| 4 | +load(":version.bzl", "version") |
4 | 5 |
|
5 | | -def _parse_requires_python(requires_python): |
6 | | - """Parse and validate the requires-python field. |
| 6 | +def read_pyproject(module_ctx, pyproject): |
| 7 | + """Read a pyproject.toml file and return the relevant fields. |
| 8 | +
|
| 9 | + The file is parsed with a pure-Starlark TOML decoder; no Python |
| 10 | + interpreter is required. The raw `requires-python` value is returned |
| 11 | + as-is so that callers can decide how to interpret it. |
7 | 12 |
|
8 | 13 | Args: |
9 | | - requires_python: The raw requires-python string from pyproject.toml. |
| 14 | + module_ctx: the module extension context (needs `path` and `read`). |
| 15 | + pyproject: {type}`Label` pointing at the pyproject.toml file. |
10 | 16 |
|
11 | 17 | Returns: |
12 | | - The bare version string (e.g. "3.13.9"). |
| 18 | + {type}`struct` with the attributes: |
| 19 | + * `requires_python`: {type}`str | None` the raw `requires-python` |
| 20 | + value (e.g. `"==3.13.9"`), or `None` if it is not set. |
13 | 21 | """ |
14 | | - if not requires_python.startswith("=="): |
15 | | - fail("requires-python must use '==' for exact version, got: {}".format(requires_python)) |
16 | | - |
17 | | - bare_version = requires_python[2:].strip() |
| 22 | + data = toml.decode(module_ctx.read(module_ctx.path(pyproject), watch = "yes")) |
| 23 | + return struct( |
| 24 | + requires_python = data.get("project", {}).get("requires-python"), |
| 25 | + ) |
18 | 26 |
|
19 | | - # Validate X.Y.Z format |
20 | | - parts = bare_version.split(".") |
21 | | - if len(parts) != 3: |
22 | | - fail("requires-python must be in X.Y.Z format, got: {}".format(bare_version)) |
23 | | - for part in parts: |
24 | | - if not part.isdigit(): |
25 | | - fail("requires-python must be in X.Y.Z format, got: {}".format(bare_version)) |
| 27 | +def version_from_requires_python(requires_python): |
| 28 | + """Derive a concrete Python version from a `requires-python` value. |
26 | 29 |
|
27 | | - return bare_version |
28 | | - |
29 | | -def read_pyproject_version(module_ctx, pyproject_label, logger = None): |
30 | | - """Reads Python version from pyproject.toml if requested. |
| 30 | + Currently only an exact `==X.Y.Z` specifier is supported. The value is |
| 31 | + validated and normalized via {obj}`//python/private:version.bzl` so that |
| 32 | + malformed input fails in a consistent way. Broader specifier support |
| 33 | + (e.g. `>=`, `X.Y`) can be layered on here in the future. |
31 | 34 |
|
32 | 35 | Args: |
33 | | - module_ctx: The module_ctx object from the module extension. |
34 | | - pyproject_label: Label pointing to the pyproject.toml file, or None. |
35 | | - logger: Optional logger instance for informational messages. |
| 36 | + requires_python: {type}`str` the raw `requires-python` value. |
36 | 37 |
|
37 | 38 | Returns: |
38 | | - The Python version string (e.g. "3.13.9") or None if pyproject_label is None. |
| 39 | + {type}`str` the normalized version string (e.g. `"3.13.9"`). |
39 | 40 | """ |
40 | | - if not pyproject_label: |
41 | | - return None |
42 | | - |
43 | | - pyproject_path = module_ctx.path(pyproject_label) |
44 | | - module_ctx.read(pyproject_path, watch = "yes") |
45 | | - |
46 | | - toml2json = module_ctx.path(_TOML2JSON) |
47 | | - result = module_ctx.execute([ |
48 | | - "python3", |
49 | | - str(toml2json), |
50 | | - str(pyproject_path), |
51 | | - ]) |
52 | | - |
53 | | - if result.return_code != 0: |
54 | | - fail("Failed to parse pyproject.toml: " + result.stderr) |
55 | | - |
56 | | - data = json.decode(result.stdout) |
57 | | - requires_python = data.get("project", {}).get("requires-python") |
58 | | - if not requires_python: |
59 | | - fail("pyproject.toml must contain [project] requires-python field") |
60 | | - |
61 | | - version = _parse_requires_python(requires_python) |
| 41 | + if not requires_python.startswith("=="): |
| 42 | + fail("`requires-python` must pin an exact version with `==`, got: {}".format(requires_python)) |
62 | 43 |
|
63 | | - if logger: |
64 | | - logger.info(lambda: "Read Python version {} from {}".format(version, pyproject_label)) |
| 44 | + bare_version = requires_python[len("=="):].strip() |
65 | 45 |
|
66 | | - return version |
| 46 | + # Parse strictly so malformed versions fail cleanly, then normalize. |
| 47 | + version.parse(bare_version, strict = True) |
| 48 | + return version.normalize(bare_version) |
0 commit comments