From 0dc65ed9d29d911d50b4998be7fbd63553513b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Tue, 25 Feb 2025 18:56:19 +0100 Subject: [PATCH 1/4] Support running `conda-smithy lint` in feedstock directory Extend the default behavior of `conda-smithy lint` to detect if a feedstock directory has been passed in place of the recipe directory (e.g. by running it with no paths specified), and handle the paths appropriately. The new logic covers three possible scenarios: 1. If `--feedstock-directory` is passed, everything works as before. 2. If not, the specified directory is checked for `meta.yaml` and `recipe.yaml`, also as before. 3. If neither exists, the specified directory is checked for `conda-forge.yml`. If it exists, it set to be the feedstock directory, and the file is parsed to determine the correct recipe subdirectory. This is primarily meant to address my common mistake of running: conda smithy lint in the feedstock directory, which can lead to pretty confusing error messages, particularly if the feedstock is using v1 recipes, and smithy says it can't find `recipe/meta.yaml` -- and you start wondering whether you've made a typo in `conda-forge.yml` or what. This is an alternative to #2249. --- conda_smithy/lint_recipe.py | 46 +++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py index d647837a9..ef77ee958 100644 --- a/conda_smithy/lint_recipe.py +++ b/conda_smithy/lint_recipe.py @@ -8,7 +8,7 @@ from inspect import cleandoc from pathlib import Path from textwrap import indent -from typing import Any, Optional +from typing import Any, Optional, Tuple import github import github.Auth @@ -722,19 +722,47 @@ def _format_validation_msg(error: jsonschema.ValidationError): ) -def main( - recipe_dir, conda_forge=False, return_hints=False, feedstock_dir=None -): +def find_recipe_directory( + recipe_dir: str, + feedstock_dir: Optional[str], +) -> Tuple[str, str]: + """Find recipe directory and build tool""" + recipe_dir = os.path.abspath(recipe_dir) build_tool = CONDA_BUILD_TOOL - if feedstock_dir: + + # The logic below: + # 1. If `--feedstock-dir` is not specified, try looking for `recipe.yaml` + # or `meta.yaml` in the specified recipe directory. + # 2. If there is none, look for `conda-forge.yml` -- perhaps the user + # passed feedstock directory instead. In that case, obtain + # the recipe directory from `conda-forge.yml`. + + if feedstock_dir is None: + if os.path.exists(os.path.join(recipe_dir, "recipe.yaml")): + return (recipe_dir, RATTLER_BUILD_TOOL) + elif os.path.exists(os.path.join(recipe_dir, "meta.yaml")): + return (recipe_dir, CONDA_BUILD_TOOL) + elif os.path.exists(os.path.join(recipe_dir, "conda-forge.yml")): + # passthrough to the feedstock_dir logic below + feedstock_dir = recipe_dir + recipe_dir = None + + if feedstock_dir is not None: feedstock_dir = os.path.abspath(feedstock_dir) forge_config = _read_forge_config(feedstock_dir) if forge_config.get("conda_build_tool", "") == RATTLER_BUILD_TOOL: build_tool = RATTLER_BUILD_TOOL - else: - if os.path.exists(os.path.join(recipe_dir, "recipe.yaml")): - build_tool = RATTLER_BUILD_TOOL + if recipe_dir is None: + recipe_dir = os.path.join(feedstock_dir, forge_config.get("recipe_dir", "recipe")) + + return (recipe_dir, build_tool) + + +def main( + recipe_dir, conda_forge=False, return_hints=False, feedstock_dir=None +): + recipe_dir, build_tool = find_recipe_directory(recipe_dir, feedstock_dir) if build_tool == RATTLER_BUILD_TOOL: recipe_file = os.path.join(recipe_dir, "recipe.yaml") @@ -743,7 +771,7 @@ def main( if not os.path.exists(recipe_file): raise OSError( - f"Feedstock has no recipe/{os.path.basename(recipe_file)}" + f"No recipe file found in {recipe_dir}" ) if build_tool == CONDA_BUILD_TOOL: From d5ca21abf9fdf081bbf1b2396fbad83e548836e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 26 Feb 2025 15:04:27 +0100 Subject: [PATCH 2/4] Fix pre-commit issues --- conda_smithy/lint_recipe.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py index ef77ee958..25221f7f4 100644 --- a/conda_smithy/lint_recipe.py +++ b/conda_smithy/lint_recipe.py @@ -8,7 +8,7 @@ from inspect import cleandoc from pathlib import Path from textwrap import indent -from typing import Any, Optional, Tuple +from typing import Any, Optional import github import github.Auth @@ -725,7 +725,7 @@ def _format_validation_msg(error: jsonschema.ValidationError): def find_recipe_directory( recipe_dir: str, feedstock_dir: Optional[str], -) -> Tuple[str, str]: +) -> tuple[str, str]: """Find recipe directory and build tool""" recipe_dir = os.path.abspath(recipe_dir) @@ -754,7 +754,9 @@ def find_recipe_directory( if forge_config.get("conda_build_tool", "") == RATTLER_BUILD_TOOL: build_tool = RATTLER_BUILD_TOOL if recipe_dir is None: - recipe_dir = os.path.join(feedstock_dir, forge_config.get("recipe_dir", "recipe")) + recipe_dir = os.path.join( + feedstock_dir, forge_config.get("recipe_dir", "recipe") + ) return (recipe_dir, build_tool) @@ -770,9 +772,7 @@ def main( recipe_file = os.path.join(recipe_dir, "meta.yaml") if not os.path.exists(recipe_file): - raise OSError( - f"No recipe file found in {recipe_dir}" - ) + raise OSError(f"No recipe file found in {recipe_dir}") if build_tool == CONDA_BUILD_TOOL: with open(recipe_file, encoding="utf-8") as fh: From a3d004e2e557b8030fa31dab8c8cfb1a389932d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 26 Feb 2025 15:57:10 +0100 Subject: [PATCH 3/4] Add tests --- tests/test_lint_recipe.py | 107 +++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py index ef8264e8a..d11962d4a 100644 --- a/tests/test_lint_recipe.py +++ b/tests/test_lint_recipe.py @@ -13,7 +13,11 @@ import conda_smithy.lint_recipe as linter from conda_smithy.linter import hints -from conda_smithy.linter.utils import VALID_PYTHON_BUILD_BACKENDS +from conda_smithy.linter.utils import ( + CONDA_BUILD_TOOL, + RATTLER_BUILD_TOOL, + VALID_PYTHON_BUILD_BACKENDS, +) from conda_smithy.utils import get_yaml, render_meta_yaml _thisdir = os.path.abspath(os.path.dirname(__file__)) @@ -4259,5 +4263,106 @@ def test_bad_specs_report(tmp_path, spec, ok): assert all("has some malformed specs" not in hint for hint in hints) is ok +@pytest.mark.parametrize( + "create_recipe_file,directory_param,expected_tool", + [ + # recipe path passed + (None, "recipe", CONDA_BUILD_TOOL), + ("meta.yaml", "recipe", CONDA_BUILD_TOOL), + ("recipe.yaml", "recipe", RATTLER_BUILD_TOOL), + # no conda-forge.yml, incorrect path passed + (None, ".", CONDA_BUILD_TOOL), + ("meta.yaml", ".", CONDA_BUILD_TOOL), + ("recipe.yaml", ".", CONDA_BUILD_TOOL), + ], +) +def test_find_recipe_directory( + tmp_path, create_recipe_file, directory_param, expected_tool +): + """ + Test ``find_recipe_directory()`` without ``conda-forge.yml`` + + When ``find_recipe_directory()`` is passed a directory with + no ``conda-forge.yml``, and no ``--feedstock-directory`` is passed, + it should just return the input directory and guess tool from files + inside it. + """ + + tmp_path.joinpath("recipe").mkdir() + if create_recipe_file is not None: + tmp_path.joinpath("recipe", create_recipe_file).touch() + + assert linter.find_recipe_directory( + str(tmp_path / directory_param), None + ) == (str(tmp_path / directory_param), expected_tool) + + +@pytest.mark.parametrize( + "build_tool", [None, CONDA_BUILD_TOOL, RATTLER_BUILD_TOOL] +) +@pytest.mark.parametrize("recipe_dir", [None, "foo", "recipe"]) +def test_find_recipe_directory_via_conda_forge_yml( + tmp_path, build_tool, recipe_dir +): + """ + Test ``find_recipe_directory()`` with ``conda-forge.yml`` + + When ``find_recipe_directory()`` is passed a directory with + ``conda-forge.yml``, and no ``--feedstock-directory``, it should read + both the recipe directory and the tool type from it. + """ + + # create all files to verify that format is taken from conda-forge.yml + tmp_path.joinpath("recipe").mkdir() + tmp_path.joinpath("recipe", "meta.yaml").touch() + tmp_path.joinpath("recipe", "recipe.yaml").touch() + + with tmp_path.joinpath("conda-forge.yml").open("w") as f: + if build_tool is not None: + f.write(f"conda_build_tool: {build_tool}\n") + if recipe_dir is not None: + f.write(f"recipe_dir: {recipe_dir}\n") + + assert linter.find_recipe_directory(str(tmp_path), None) == ( + str(tmp_path / (recipe_dir or "recipe")), + build_tool or CONDA_BUILD_TOOL, + ) + + +@pytest.mark.parametrize( + "build_tool", [None, CONDA_BUILD_TOOL, RATTLER_BUILD_TOOL] +) +@pytest.mark.parametrize("yaml_recipe_dir", [None, "foo", "recipe"]) +@pytest.mark.parametrize("directory_param", [".", "recipe"]) +def test_find_recipe_directory_with_feedstock_dir( + tmp_path, build_tool, yaml_recipe_dir, directory_param +): + """ + Test ``find_recipe_directory()`` with ``--feedstock-directory`` + + When ``find_recipe_directory()`` is passed a ``--feedstock-directory``, + it should read the tool type from it, but use the passed recipe + directory. + """ + + # create all files to verify that format is taken from conda-forge.yml + tmp_path.joinpath("recipe").mkdir() + tmp_path.joinpath("recipe", "meta.yaml").touch() + tmp_path.joinpath("recipe", "recipe.yaml").touch() + + with tmp_path.joinpath("conda-forge.yml").open("w") as f: + if build_tool is not None: + f.write(f"conda_build_tool: {build_tool}\n") + if yaml_recipe_dir is not None: + f.write(f"recipe_dir: {yaml_recipe_dir}\n") + + assert linter.find_recipe_directory( + str(tmp_path / directory_param), str(tmp_path) + ) == ( + str(tmp_path / directory_param), + build_tool or CONDA_BUILD_TOOL, + ) + + if __name__ == "__main__": unittest.main() From 7a32b115f06620ca633abb7f44acfb633119a864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Wed, 26 Feb 2025 16:08:39 +0100 Subject: [PATCH 4/4] Add a news entry --- news/2250-recipe-dir-param.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 news/2250-recipe-dir-param.rst diff --git a/news/2250-recipe-dir-param.rst b/news/2250-recipe-dir-param.rst new file mode 100644 index 000000000..c4c3cbcc1 --- /dev/null +++ b/news/2250-recipe-dir-param.rst @@ -0,0 +1,24 @@ +**Added:** + +* + +**Changed:** + +* ``conda-smithy lint`` now can be run with the feedstock directory instead of + the recipe subdirectory. (#2250) + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +*