-
Notifications
You must be signed in to change notification settings - Fork 74
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
add v2 recipe editing capabilities for bumping version & build number #2920
base: main
Are you sure you want to change the base?
Changes from all commits
dc9ef01
e5751ef
4c83a3b
ee9497f
eed8c2e
b818676
d109abd
b7f6136
58596d2
4b3d827
d0681fb
1f0fba7
0882421
7fee6de
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,4 @@ | ||
from .build_number import update_build_number | ||
from .version import update_version | ||
|
||
__all__ = ["update_build_number", "update_version"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import TYPE_CHECKING, Any, Literal | ||
|
||
from conda_forge_tick.update_recipe.v2.yaml import _dump_yaml_to_str, _load_yaml | ||
|
||
if TYPE_CHECKING: | ||
from pathlib import Path | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
HashType = Literal["md5", "sha256"] | ||
|
||
|
||
def _update_build_number_in_context( | ||
recipe: dict[str, Any], new_build_number: int | ||
) -> bool: | ||
for key in recipe.get("context", {}): | ||
if key.startswith("build_") or key == "build": | ||
recipe["context"][key] = new_build_number | ||
return True | ||
return False | ||
|
||
|
||
def _update_build_number_in_recipe( | ||
recipe: dict[str, Any], new_build_number: int | ||
) -> bool: | ||
is_modified = False | ||
if "build" in recipe and "number" in recipe["build"]: | ||
recipe["build"]["number"] = new_build_number | ||
is_modified = True | ||
|
||
if "outputs" in recipe: | ||
for output in recipe["outputs"]: | ||
if "build" in output and "number" in output["build"]: | ||
output["build"]["number"] = new_build_number | ||
is_modified = True | ||
|
||
return is_modified | ||
|
||
|
||
def update_build_number(file: Path, new_build_number: int = 0) -> str: | ||
""" | ||
Update the build number in the recipe file. | ||
|
||
Arguments: | ||
---------- | ||
* `file` - The path to the recipe file. | ||
* `new_build_number` - The new build number to use. (default: 0) | ||
|
||
Returns: | ||
-------- | ||
* The updated recipe as a string. | ||
""" | ||
data = _load_yaml(file) | ||
build_number_modified = _update_build_number_in_context(data, new_build_number) | ||
if not build_number_modified: | ||
_update_build_number_in_recipe(data, new_build_number) | ||
|
||
return _dump_yaml_to_str(data) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
from __future__ import annotations | ||
|
||
from typing import TYPE_CHECKING, Any, Generic, List, TypeVar, Union, cast | ||
|
||
if TYPE_CHECKING: | ||
from collections.abc import Callable, Generator | ||
|
||
T = TypeVar("T") | ||
K = TypeVar("K") | ||
|
||
|
||
class IfStatement(Generic[T]): | ||
if_: Any | ||
then: T | list[T] | ||
else_: T | list[T] | None | ||
|
||
|
||
ConditionalList = Union[T, IfStatement[T], List[Union[T, IfStatement[T]]]] | ||
|
||
|
||
def visit_conditional_list( # noqa: C901 | ||
value: T | IfStatement[T] | list[T | IfStatement[T]], | ||
evaluator: Callable[[Any], bool] | None = None, | ||
) -> Generator[T]: | ||
""" | ||
A function that yields individual branches of a conditional list. | ||
|
||
Arguments | ||
--------- | ||
* `value` - The value to evaluate | ||
* `evaluator` - An optional evaluator to evaluate the `if` expression. | ||
|
||
Returns | ||
------- | ||
A generator that yields the individual branches. | ||
""" | ||
|
||
def yield_from_list(value: list[K] | K) -> Generator[K]: | ||
if isinstance(value, list): | ||
yield from value | ||
else: | ||
yield value | ||
|
||
if not isinstance(value, list): | ||
value = [value] | ||
|
||
for element in value: | ||
if isinstance(element, dict): | ||
if (expr := element.get("if", None)) is not None: | ||
then = element.get("then") | ||
otherwise = element.get("else") | ||
# Evaluate the if expression if the evaluator is provided | ||
if evaluator: | ||
if evaluator(expr): | ||
yield from yield_from_list(then) | ||
elif otherwise: | ||
yield from yield_from_list(otherwise) | ||
# Otherwise, just yield the branches | ||
else: | ||
yield from yield_from_list(then) | ||
if otherwise: | ||
yield from yield_from_list(otherwise) | ||
else: | ||
# In this case its not an if statement | ||
yield cast(T, element) | ||
# If the element is not a dictionary, just yield it | ||
else: | ||
# (tim) I get a pyright error here, but I don't know how to fix it | ||
yield cast(T, element) | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import jinja2 | ||
|
||
|
||
def load_recipe_context( | ||
context: dict[str, str], jinja_env: jinja2.Environment | ||
) -> dict[str, str]: | ||
""" | ||
Load all string values from the context dictionary as Jinja2 templates. | ||
Use linux-64 as default target_platform, build_platform, and mpi. | ||
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. I'm concerned that assuming linux as the default will result in invalid updates for some recipes. 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. I understand where you are coming from but we do ignore the selectors in the version updating code (e.g. we go down both branches, However, we do use the value for templating, so people could theoretically do something like source:
url: https://foo.${{ "linux" if linux else "bla" }}/...
sha256: ${{ "foo" ... But that doesn't seem very likely and also buggy so I don't think we'll see that a lot in the wild. 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. Are folks allowed to use if..then in the context section to change the values of variables? 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. I believe that doesn't work in the context section. However you can use templating and inline Jinja expressions, such as: context:
version: "1.2.3"
version_string: "v${{ version if linux else "foo" }}" 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. right so we need to make this correct. 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. That won't change other items that might be platform dependent in the context section. 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. But the SHA hash will make it impossible, thankfully :) 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. Nope. Folks could define parts of urls in the context section that are platform dependent that are not hashes. So when the bot comes through to render those urls and parts, it will compute the wrong hash since it might have a valid, but incorrect url for that platform. 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. @beckermr - i sorta get where you come from but is it really necessary to care about all of these edge cases on day one? It is also not a very clever way to write the recipe. You would have to "double-if" the source. E.g. context:
foo: ${{ "bla" if linux else "foo" }}
source:
- if: linux
then:
url: ${{ foo }}
sha256: 1abcd
else:
url: ${{ foo }}
sha256: 2bcde So I don't think many people would wanna write their sources like this (doesn't make much logical sense). 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. Yes we should care. My suggested procedure for this work was to abstract the current version update algorithm to be independent of the recipe format backend and then use that. It should handle most, if not all, of the cases I am bringing up. |
||
""" | ||
# Process each key-value pair in the dictionary | ||
for key, value in context.items(): | ||
# If the value is a string, render it as a template | ||
if isinstance(value, str): | ||
template = jinja_env.from_string(value) | ||
rendered_value = template.render(context) | ||
context[key] = rendered_value | ||
|
||
return context |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .jinja import jinja_env | ||
|
||
__all__ = ["jinja_env"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from __future__ import annotations | ||
|
||
from conda_forge_tick.update_recipe.v2.jinja.utils import _MissingUndefined | ||
|
||
|
||
def _version_to_build_string(some_string: str | _MissingUndefined) -> str: | ||
""" | ||
Converts some version by removing the . character and returning only the first two elements of the version. | ||
If piped value is undefined, it returns the undefined value as is. | ||
""" | ||
if isinstance(some_string, _MissingUndefined): | ||
return f"{some_string._undefined_name}_version_to_build_string" # noqa: SLF001 | ||
# We first split the string by whitespace and take the first part | ||
split = some_string.split()[0] if some_string.split() else some_string | ||
# We then split the string by . and take the first two parts | ||
parts = split.split(".") | ||
major = parts[0] if len(parts) > 0 else "" | ||
minor = parts[1] if len(parts) > 1 else "" | ||
return f"{major}{minor}" | ||
|
||
|
||
def _bool(value: str) -> bool: | ||
return bool(value) | ||
|
||
|
||
def _split(s: str, sep: str | None = None) -> list[str]: | ||
"""Filter that split a string by a separator""" | ||
return s.split(sep) | ||
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.
People can and have mixed setting build numbers in both the jinja2 vars / context and explicitly in the recipe. I know it is awful. We should update the code to do both and check if the build number in the non-context part is templated or not before updating.