Skip to content

Commit

Permalink
Support running conda-smithy lint in feedstock directory (#2250)
Browse files Browse the repository at this point in the history
* 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.

* Fix pre-commit issues

* Add tests

* Add a news entry

---------

Co-authored-by: Matthew R. Becker <[email protected]>
  • Loading branch information
mgorny and beckermr authored Feb 27, 2025
1 parent fcd784c commit 355cfa4
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 11 deletions.
48 changes: 38 additions & 10 deletions conda_smithy/lint_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,29 +722,57 @@ 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")
else:
recipe_file = os.path.join(recipe_dir, "meta.yaml")

if not os.path.exists(recipe_file):
raise OSError(
f"Feedstock has no recipe/{os.path.basename(recipe_file)}"
)
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:
Expand Down
24 changes: 24 additions & 0 deletions news/2250-recipe-dir-param.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
**Added:**

* <news item>

**Changed:**

* ``conda-smithy lint`` now can be run with the feedstock directory instead of
the recipe subdirectory. (#2250)

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
107 changes: 106 additions & 1 deletion tests/test_lint_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__))
Expand Down Expand Up @@ -4257,5 +4261,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()

0 comments on commit 355cfa4

Please sign in to comment.