-
-
Notifications
You must be signed in to change notification settings - Fork 699
feat: enable pyproject.toml as single source of truth for Python version #3514
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| (pip,python) Added `pyproject_toml` attribute to `pip.default()` and `python.defaults()` | ||
| to read the default Python version from the `requires-python` field of `pyproject.toml`. |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,6 +20,7 @@ load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") | |||||||||||||
| load("@toml.bzl", "toml") | ||||||||||||||
| load("//python/private:auth.bzl", "AUTH_ATTRS") | ||||||||||||||
| load("//python/private:normalize_name.bzl", "normalize_name") | ||||||||||||||
| load("//python/private:pyproject_utils.bzl", "read_pyproject", "version_from_requires_python") | ||||||||||||||
| load("//python/private:repo_utils.bzl", "repo_utils") | ||||||||||||||
| load(":hub_builder.bzl", "hub_builder") | ||||||||||||||
| load(":hub_repository.bzl", "hub_repository", "whl_config_settings_to_json") | ||||||||||||||
|
|
@@ -208,6 +209,7 @@ def build_config( | |||||||||||||
| default_hub = None | ||||||||||||||
| defaults = { | ||||||||||||||
| "platforms": default_platforms(), | ||||||||||||||
| "python_version": None, | ||||||||||||||
| } | ||||||||||||||
| for mod in module_ctx.modules: | ||||||||||||||
| if not (mod.is_root or mod.name == "rules_python"): | ||||||||||||||
|
|
@@ -219,6 +221,11 @@ def build_config( | |||||||||||||
| if default_hub: | ||||||||||||||
| fail("Duplicate pip.default tag: only one explicit default PyPI hub is allowed.") | ||||||||||||||
| default_hub = tag.default_hub | ||||||||||||||
| pyproject_toml = tag.pyproject_toml | ||||||||||||||
| if pyproject_toml: | ||||||||||||||
| pyproject = read_pyproject(module_ctx, pyproject_toml) | ||||||||||||||
| if pyproject.requires_python: | ||||||||||||||
| defaults["python_version"] = version_from_requires_python(pyproject.requires_python) | ||||||||||||||
|
|
||||||||||||||
| platform = tag.platform | ||||||||||||||
| if platform: | ||||||||||||||
|
|
@@ -256,6 +263,7 @@ def build_config( | |||||||||||||
| default_hub = default_hub, | ||||||||||||||
| index_url = defaults.get("index_url", "https://pypi.org/simple").rstrip("/"), | ||||||||||||||
| netrc = defaults.get("netrc", None), | ||||||||||||||
| python_version = defaults.get("python_version", None), | ||||||||||||||
| platforms = { | ||||||||||||||
| name: _plat(**values) | ||||||||||||||
| for name, values in defaults["platforms"].items() | ||||||||||||||
|
|
@@ -359,6 +367,15 @@ You cannot use both the additive_build_content and additive_build_content_file a | |||||||||||||
|
|
||||||||||||||
| for mod in module_ctx.modules: | ||||||||||||||
| for pip_attr in mod.tags.parse: | ||||||||||||||
| python_version = pip_attr.python_version | ||||||||||||||
| if not python_version and pip_attr.pyproject_toml: | ||||||||||||||
| pyproject = read_pyproject(module_ctx, pip_attr.pyproject_toml) | ||||||||||||||
| if pyproject.requires_python: | ||||||||||||||
| python_version = version_from_requires_python(pyproject.requires_python) | ||||||||||||||
| python_version = python_version or config.python_version | ||||||||||||||
| if not python_version: | ||||||||||||||
| _fail("pip.parse() requires one of `python_version`, `pyproject_toml`, or `pip.default(pyproject_toml=...)` to be set") | ||||||||||||||
|
|
||||||||||||||
| hub_name = pip_attr.hub_name | ||||||||||||||
| if hub_name == "pypi": | ||||||||||||||
| if is_pypi_hub_reserved: | ||||||||||||||
|
|
@@ -422,6 +439,7 @@ You cannot use both the additive_build_content and additive_build_content_file a | |||||||||||||
| builder.pip_parse( | ||||||||||||||
| module_ctx, | ||||||||||||||
| pip_attr = pip_attr, | ||||||||||||||
| python_version = python_version, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| # Keeps track of all the hub's whl repos across the different versions. | ||||||||||||||
|
|
@@ -699,6 +717,23 @@ If you are defining custom platforms in your project and don't want things to cl | |||||||||||||
| [isolation] feature. | ||||||||||||||
|
|
||||||||||||||
| [isolation]: https://bazel.build/rules/lib/globals/module#use_extension.isolate | ||||||||||||||
| """, | ||||||||||||||
| ), | ||||||||||||||
| "pyproject_toml": attr.label( | ||||||||||||||
| mandatory = False, | ||||||||||||||
| doc = """\ | ||||||||||||||
| Label pointing to pyproject.toml file to read the default Python version from. | ||||||||||||||
| When specified, reads the `requires-python` field from pyproject.toml and uses | ||||||||||||||
| it as the default python_version for all `pip.parse()` calls that don't | ||||||||||||||
| explicitly specify one. | ||||||||||||||
|
|
||||||||||||||
| :::{note} | ||||||||||||||
| The version must be specified as `==X.Y.Z` (exact version with full semver). | ||||||||||||||
| This is designed to work with dependency management tools like Renovate. | ||||||||||||||
|
Comment on lines
+731
to
+732
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
| ::: | ||||||||||||||
|
|
||||||||||||||
| :::{versionadded} VERSION_NEXT_FEATURE | ||||||||||||||
| ::: | ||||||||||||||
| """, | ||||||||||||||
| ), | ||||||||||||||
| "whl_abi_tags": attr.string_list( | ||||||||||||||
|
|
@@ -869,15 +904,39 @@ find in case extra indexes are specified. | |||||||||||||
| """, | ||||||||||||||
| default = True, | ||||||||||||||
| ), | ||||||||||||||
| "pyproject_toml": attr.label( | ||||||||||||||
| mandatory = False, | ||||||||||||||
| doc = """\ | ||||||||||||||
| Label pointing to a pyproject.toml file to read the Python version from. | ||||||||||||||
| When specified, the `requires-python` field is used as the `python_version` | ||||||||||||||
| for this `pip.parse()` call, unless `python_version` is set explicitly. | ||||||||||||||
|
|
||||||||||||||
| :::{note} | ||||||||||||||
| The version must be specified as `==X.Y.Z` (exact version with full semver). | ||||||||||||||
| ::: | ||||||||||||||
|
|
||||||||||||||
| :::{versionadded} VERSION_NEXT_FEATURE | ||||||||||||||
| ::: | ||||||||||||||
| """, | ||||||||||||||
| ), | ||||||||||||||
| "python_version": attr.string( | ||||||||||||||
| mandatory = True, | ||||||||||||||
| mandatory = False, | ||||||||||||||
|
aignas marked this conversation as resolved.
|
||||||||||||||
| doc = """ | ||||||||||||||
| The Python version the dependencies are targetting, in Major.Minor format | ||||||||||||||
| (e.g., "3.11") or patch level granularity (e.g. "3.11.1"). | ||||||||||||||
|
|
||||||||||||||
| If an interpreter isn't explicitly provided (using `python_interpreter` or | ||||||||||||||
| `python_interpreter_target`), then the version specified here must have | ||||||||||||||
| a corresponding `python.toolchain()` configured. | ||||||||||||||
|
|
||||||||||||||
| :::{seealso} | ||||||||||||||
| The {obj}`pyproject_toml` attribute for getting the version from a project file. | ||||||||||||||
| ::: | ||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
#1708 is also an important issue to mention in this PR if we lift this restriction.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added #1708 in PR desc |
||||||||||||||
|
|
||||||||||||||
| :::{versionchanged} VERSION_NEXT_FEATURE | ||||||||||||||
| No longer mandatory if the {obj}`pyproject_toml` attribute or | ||||||||||||||
| {obj}`pip.default.pyproject_toml` is specified. | ||||||||||||||
| ::: | ||||||||||||||
| """, | ||||||||||||||
| ), | ||||||||||||||
| "simpleapi_skip": attr.string_list( | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,51 @@ | ||||||||||||
| """Utilities for reading values from pyproject.toml.""" | ||||||||||||
|
|
||||||||||||
| load("@toml.bzl", "toml") | ||||||||||||
| load(":version.bzl", "version") | ||||||||||||
|
|
||||||||||||
| def read_pyproject(module_ctx, pyproject): | ||||||||||||
| """Read a pyproject.toml file and return the relevant fields. | ||||||||||||
|
|
||||||||||||
| The file is parsed with a pure-Starlark TOML decoder; no Python | ||||||||||||
| interpreter is required. The raw `requires-python` value is returned | ||||||||||||
| as-is so that callers can decide how to interpret it. | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| module_ctx: the module extension context (needs `path` and `read`). | ||||||||||||
| pyproject: {type}`Label` pointing at the pyproject.toml file. | ||||||||||||
|
|
||||||||||||
| Returns: | ||||||||||||
| {type}`struct` with the attributes: | ||||||||||||
| * `requires_python`: {type}`str | None` the raw `requires-python` | ||||||||||||
| value (e.g. `"==3.13.9"`), or `None` if it is not set. | ||||||||||||
| """ | ||||||||||||
| data = toml.decode(module_ctx.read(module_ctx.path(pyproject), watch = "yes")) | ||||||||||||
| return struct( | ||||||||||||
| requires_python = data.get("project", {}).get("requires-python"), | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| def version_from_requires_python(requires_python): | ||||||||||||
| """Derive a concrete Python version from a `requires-python` value. | ||||||||||||
|
|
||||||||||||
| Currently only an exact `==X.Y.Z` specifier is supported. The value is | ||||||||||||
| validated and normalized via {obj}`//python/private:version.bzl` so that | ||||||||||||
| malformed input fails in a consistent way. Broader specifier support | ||||||||||||
| (e.g. `>=`, `X.Y`) can be layered on here in the future. | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| requires_python: {type}`str` the raw `requires-python` value. | ||||||||||||
|
|
||||||||||||
| Returns: | ||||||||||||
| {type}`str` the normalized version string (e.g. `"3.13.9"`). | ||||||||||||
| """ | ||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| if not requires_python: | ||||||||||||
| fail("`requires-python` must be specified") | ||||||||||||
|
|
||||||||||||
| if not requires_python.startswith("=="): | ||||||||||||
| fail("`requires-python` must pin an exact version with `==`, got: {}".format(requires_python)) | ||||||||||||
|
|
||||||||||||
| bare_version = requires_python[len("=="):].strip() | ||||||||||||
|
|
||||||||||||
| # Parse strictly so malformed versions fail cleanly, then normalize. | ||||||||||||
| version.parse(bare_version, strict = True) | ||||||||||||
| return version.normalize(bare_version) | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for also allowing users specify
pyproject_tomlvia thepip.parse.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added