Skip to content
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 "Set the /build/number back to 0" logic to bump-recipe #231

Merged
merged 8 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 44 additions & 19 deletions conda_recipe_manager/commands/bump_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,48 @@
from conda_recipe_manager.parser.recipe_parser import RecipeParser
from conda_recipe_manager.types import JsonPatchType


# TODO Improve. In order for `click` to play nice with `pyfakefs`, we set `path_type=str` and delay converting to a
# `Path` instance. This is caused by how `click` uses decorators. See these links for more detail:
# - https://pytest-pyfakefs.readthedocs.io/en/latest/troubleshooting.html#pathlib-path-objects-created-outside-of-tests
# - https://github.com/pytest-dev/pyfakefs/discussions/605


def _get_required_patch_blob(recipe_parser: RecipeParser, increment_build_num: bool) -> JsonPatchType:
"""
Returns the required JSON Patch Blob

:param recipe_parser: Recipe file to update.
:param increment_build_num: Increments the `/build/number` field by 1 if set to `True`. Otherwise resets to 0.
:returns: A JSON Patch blob to add or modify the build number
"""

# Try to get "build" key from the recipe, exit if not found
try:
recipe_parser.get_value("/build")
except KeyError:
print_err("`/build` key could not be found in the recipe.")
sys.exit(ExitCode.ILLEGAL_OPERATION)

# If build key is found, try to get build/number key in case of `build_num` set to false, `build/number` key will be
# added and set to zero when `build_num` is set to true, throw error and sys.exit()

# TODO use contains_value() instead of try catch
try:
build_number = recipe_parser.get_value("/build/number")
if increment_build_num:
if not isinstance(build_number, int):
print_err("Build number is not an integer.")
sys.exit(ExitCode.ILLEGAL_OPERATION)

return cast(JsonPatchType, {"op": "replace", "path": "/build/number", "value": build_number + 1})
except KeyError:
if increment_build_num:
print_err("`/build/number` key could not be found in the recipe.")
sys.exit(ExitCode.ILLEGAL_OPERATION)

return cast(JsonPatchType, {"op": "add", "path": "/build/number", "value": 0})


@click.command(short_help="Bumps a recipe file to a new version.")
@click.argument("recipe_file_path", type=click.Path(exists=True, path_type=str))
@click.option(
Expand All @@ -45,23 +82,11 @@ def bump_recipe(recipe_file_path: str, build_num: bool) -> None:
print_err("An error occurred while parsing the recipe file contents.")
sys.exit(ExitCode.PARSE_EXCEPTION)

if build_num:
try:
build_number = recipe_parser.get_value("/build/number")
except KeyError:
print_err("`/build/number` key could not be found in the recipe.")
sys.exit(ExitCode.ILLEGAL_OPERATION)

if not isinstance(build_number, int):
print_err("Build number is not an integer.")
sys.exit(ExitCode.ILLEGAL_OPERATION)
required_patch_blob = _get_required_patch_blob(recipe_parser, build_num)

required_patch_blob = cast(JsonPatchType, {"op": "replace", "path": "/build/number", "value": build_number + 1})

if not recipe_parser.patch(required_patch_blob):
print_err(f"Couldn't perform the patch: {required_patch_blob}.")
sys.exit(ExitCode.PARSE_EXCEPTION)
if not recipe_parser.patch(required_patch_blob):
print_err(f"Couldn't perform the patch: {required_patch_blob}.")
sys.exit(ExitCode.PARSE_EXCEPTION)

Path(recipe_file_path).write_text(recipe_parser.render(), encoding="utf-8")
sys.exit(ExitCode.SUCCESS)
print_err("Sorry, the default bump behaviour has not been implemented yet.")
Path(recipe_file_path).write_text(recipe_parser.render(), encoding="utf-8")
sys.exit(ExitCode.SUCCESS)
1 change: 1 addition & 0 deletions conda_recipe_manager/parser/recipe_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@ def contains_value(self, path: str) -> bool:
"""
Determines if a value (via a path) is contained in this recipe. This also allows the caller to determine if a
path exists.

:param path: JSON patch (RFC 6902)-style path to a value.
:returns: True if the path exists. False otherwise.
"""
Expand Down
67 changes: 57 additions & 10 deletions tests/commands/test_bump_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
:Description: Tests the `bump-recipe` CLI
"""

from typing import Final

import pytest
from click.testing import CliRunner
from pyfakefs.fake_filesystem import FakeFilesystem
Expand All @@ -21,17 +23,23 @@ def test_usage() -> None:


@pytest.mark.parametrize(
"recipe_file, expected_recipe_file",
"recipe_file, increment_build_num, expected_recipe_file",
[
("simple-recipe.yaml", "bump_recipe/build_num_1.yaml"),
("bump_recipe/build_num_1.yaml", "bump_recipe/build_num_2.yaml"),
("bump_recipe/build_num_42.yaml", "bump_recipe/build_num_43.yaml"),
("bump_recipe/build_num_-1.yaml", "simple-recipe.yaml"),
("simple-recipe.yaml", True, "bump_recipe/build_num_1.yaml"),
("simple-recipe.yaml", False, "simple-recipe.yaml"),
("bump_recipe/build_num_1.yaml", True, "bump_recipe/build_num_2.yaml"),
("bump_recipe/build_num_1.yaml", False, "simple-recipe.yaml"),
("bump_recipe/build_num_42.yaml", True, "bump_recipe/build_num_43.yaml"),
("bump_recipe/build_num_42.yaml", False, "simple-recipe.yaml"),
("bump_recipe/build_num_-1.yaml", True, "simple-recipe.yaml"),
("bump_recipe/build_num_-1.yaml", False, "simple-recipe.yaml"),
],
)
def test_bump_recipe_cli(fs: FakeFilesystem, recipe_file: str, expected_recipe_file: str) -> None:
def test_bump_recipe_cli(
fs: FakeFilesystem, recipe_file: str, increment_build_num: bool, expected_recipe_file: str
) -> None:
"""
Test for the case when build number is successfully incremented by 1.
Test that the recipe file is successfully updated/bumped.

:param fs: `pyfakefs` Fixture used to replace the file system
"""
Expand All @@ -41,7 +49,31 @@ def test_bump_recipe_cli(fs: FakeFilesystem, recipe_file: str, expected_recipe_f
recipe_file_path = get_test_path() / recipe_file
expected_recipe_file_path = get_test_path() / expected_recipe_file

result = runner.invoke(bump_recipe.bump_recipe, ["--build-num", str(recipe_file_path)])
cli_args: Final[list[str]] = (
["--build-num", str(recipe_file_path)] if increment_build_num else [str(recipe_file_path)]
)
result = runner.invoke(bump_recipe.bump_recipe, cli_args)

parser = load_recipe(recipe_file_path, RecipeParser)
expected_parser = load_recipe(expected_recipe_file_path, RecipeParser)

assert parser == expected_parser
assert result.exit_code == ExitCode.SUCCESS


def test_bump_cli_build_number_key_missing(fs: FakeFilesystem) -> None:
"""
Test that a `build: number:` key is added and set to 0 when it's missing.

:param fs: `pyfakefs` Fixture used to replace the file system
"""
runner = CliRunner()
fs.add_real_directory(get_test_path(), read_only=False)

recipe_file_path = get_test_path() / "bump_recipe/no_build_num.yaml"
expected_recipe_file_path = get_test_path() / "bump_recipe/build_num_added.yaml"

result = runner.invoke(bump_recipe.bump_recipe, [str(recipe_file_path)])

parser = load_recipe(recipe_file_path, RecipeParser)
expected_parser = load_recipe(expected_recipe_file_path, RecipeParser)
Expand All @@ -50,7 +82,7 @@ def test_bump_recipe_cli(fs: FakeFilesystem, recipe_file: str, expected_recipe_f
assert result.exit_code == ExitCode.SUCCESS


def test_bump_build_num_not_int(fs: FakeFilesystem) -> None:
def test_bump_cli_build_number_not_int(fs: FakeFilesystem) -> None:
"""
Test that the command fails gracefully case when the build number is not an integer,
and we are trying to increment it.
Expand All @@ -67,7 +99,7 @@ def test_bump_build_num_not_int(fs: FakeFilesystem) -> None:
assert result.exit_code == ExitCode.ILLEGAL_OPERATION


def test_bump_build_num_key_not_found(fs: FakeFilesystem) -> None:
def test_bump_cli_increment_build_num_key_not_found(fs: FakeFilesystem) -> None:
"""
Test that the command fails gracefully when the build number key is missing and we try to increment it's value.

Expand All @@ -80,3 +112,18 @@ def test_bump_build_num_key_not_found(fs: FakeFilesystem) -> None:
recipe_file_path = get_test_path() / "bump_recipe/no_build_num.yaml"
result = runner.invoke(bump_recipe.bump_recipe, ["--build-num", str(recipe_file_path)])
assert result.exit_code == ExitCode.ILLEGAL_OPERATION


def test_bump_cli_no_build_key_found(fs: FakeFilesystem) -> None:
"""
Test that the command fails gracefully when the build key is missing and we try to revert build number to zero.
ForgottenProgramme marked this conversation as resolved.
Show resolved Hide resolved

:param fs: `pyfakefs` Fixture used to replace the file system
"""

runner = CliRunner()
fs.add_real_directory(get_test_path(), read_only=False)

recipe_file_path = get_test_path() / "bump_recipe/no_build_key.yaml"
result = runner.invoke(bump_recipe.bump_recipe, [str(recipe_file_path)])
assert result.exit_code == ExitCode.ILLEGAL_OPERATION
53 changes: 53 additions & 0 deletions tests/test_aux_files/bump_recipe/build_num_added.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{% set zz_non_alpha_first = 42 %}
{% set name = "types-toml" %}
{% set version = "0.10.8.6" %}

package:
name: {{ name|lower }} # [unix]

build:
skip: true # [py<37]
is_true: true
number: 0

# Comment above a top-level structure
requirements:
empty_field1:
host:
- setuptools # [unix]
- fakereq # [unix] selector with comment
empty_field2: # [unix and win] # selector with comment with comment symbol
run:
- python # not a selector
empty_field3:

about:
summary: This is a small recipe for testing
description: |
This is a PEP '561 type stub package for the toml package.
It can be used by type-checking tools like mypy, pyright,
pytype, PyCharm, etc. to check code that uses toml.
license: Apache-2.0 AND MIT

multi_level:
list_1:
- foo
# Ensure a comment in a list is supported
- bar
list_2:
- cat
- bat
- mat
list_3:
- ls
- sl
- cowsay

test_var_usage:
foo: {{ version }}
bar:
- baz
- {{ zz_non_alpha_first }}
- blah
- This {{ name }} is silly
- last
48 changes: 48 additions & 0 deletions tests/test_aux_files/bump_recipe/no_build_key.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{% set zz_non_alpha_first = 42 %}
{% set name = "types-toml" %}
{% set version = "0.10.8.6" %}

package:
name: {{ name|lower }} # [unix]

# Comment above a top-level structure
requirements:
empty_field1:
host:
- setuptools # [unix]
- fakereq # [unix] selector with comment
empty_field2: # [unix and win] # selector with comment with comment symbol
run:
- python # not a selector
empty_field3:

about:
summary: This is a small recipe for testing
description: |
This is a PEP '561 type stub package for the toml package.
It can be used by type-checking tools like mypy, pyright,
pytype, PyCharm, etc. to check code that uses toml.
license: Apache-2.0 AND MIT

multi_level:
list_1:
- foo
# Ensure a comment in a list is supported
- bar
list_2:
- cat
- bat
- mat
list_3:
- ls
- sl
- cowsay

test_var_usage:
foo: {{ version }}
bar:
- baz
- {{ zz_non_alpha_first }}
- blah
- This {{ name }} is silly
- last
44 changes: 43 additions & 1 deletion tests/test_aux_files/bump_recipe/no_build_num.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,46 @@ package:

build:
skip: true # [py<37]
is_true: true
is_true: true

# Comment above a top-level structure
requirements:
empty_field1:
host:
- setuptools # [unix]
- fakereq # [unix] selector with comment
empty_field2: # [unix and win] # selector with comment with comment symbol
run:
- python # not a selector
empty_field3:

about:
summary: This is a small recipe for testing
description: |
This is a PEP '561 type stub package for the toml package.
It can be used by type-checking tools like mypy, pyright,
pytype, PyCharm, etc. to check code that uses toml.
license: Apache-2.0 AND MIT

multi_level:
list_1:
- foo
# Ensure a comment in a list is supported
- bar
list_2:
- cat
- bat
- mat
list_3:
- ls
- sl
- cowsay

test_var_usage:
foo: {{ version }}
bar:
- baz
- {{ zz_non_alpha_first }}
- blah
- This {{ name }} is silly
- last
Loading