Skip to content

Commit

Permalink
Merge pull request #2141 from conda-forge/parse-lint-hint
Browse files Browse the repository at this point in the history
feat: add lint+hint for recipe parsing
  • Loading branch information
beckermr authored Nov 19, 2024
2 parents 4c4402b + 02a1656 commit 1b800c2
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 1 deletion.
6 changes: 5 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ jobs:
environment-file: environment.yml
cache-environment: true
create-args: >-
python=3.8
python=3.11
coverage
coveralls
conda-recipe-manager
conda-souschef
conda-forge-tick
- name: install conda-smithy
run: |
conda uninstall --force --yes conda-smithy
python -m pip install -v --no-build-isolation -e .
git config --global user.email "[email protected]"
git config --global user.name "smithy"
Expand Down
17 changes: 17 additions & 0 deletions conda_smithy/lint_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
lint_package_version,
lint_pin_subpackages,
lint_recipe_have_tests,
lint_recipe_is_parsable,
lint_recipe_maintainers,
lint_recipe_name,
lint_recipe_v1_noarch_and_runtime_dependencies,
Expand Down Expand Up @@ -606,6 +607,22 @@ def run_conda_forge_specific(
hints,
)

# 11: ensure we can parse the recipe
if recipe_version == 1:
recipe_fname = os.path.join(recipe_dir or "", "recipe.yaml")
else:
recipe_fname = os.path.join(recipe_dir or "", "meta.yaml")

if os.path.exists(recipe_fname):
with open(recipe_fname) as fh:
recipe_text = fh.read()
lint_recipe_is_parsable(
recipe_text,
lints,
hints,
recipe_version=recipe_version,
)


def _format_validation_msg(error: jsonschema.ValidationError):
"""Use the data on the validation error to generate improved reporting.
Expand Down
94 changes: 94 additions & 0 deletions conda_smithy/linter/lints.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import itertools
import logging
import os
import re
import tempfile
from collections.abc import Sequence
from typing import Any, Dict, List, Literal, Optional

Expand All @@ -26,6 +28,8 @@
)
from conda_smithy.utils import get_yaml

logger = logging.getLogger(__name__)


def lint_section_order(
major_sections: List[str],
Expand Down Expand Up @@ -983,3 +987,93 @@ def sort_osx(versions):
):
if sdk_lint not in lints:
lints.append(sdk_lint)


def lint_recipe_is_parsable(
recipe_text: str,
lints: List[str],
hints: List[str],
recipe_version: int = 0,
):
parse_results = {}

if recipe_version == 0:
parse_name = "conda-forge-tick (the bot)"
try:
from conda_forge_tick.recipe_parser import CondaMetaYAML
except ImportError:
parse_results[parse_name] = None
pass
else:
try:
CondaMetaYAML(recipe_text)
except Exception as e:
logger.warning(
"Error parsing recipe with conda-forge-tick (the bot): %s",
repr(e),
exc_info=e,
)
parse_results[parse_name] = False
else:
parse_results[parse_name] = True

parse_name = "conda-souschef (grayskull)"
try:
from souschef.recipe import Recipe
except ImportError:
parse_results[parse_name] = None
pass
else:
with tempfile.TemporaryDirectory() as tmpdir:
recipe_file = os.path.join(tmpdir, "meta.yaml")
with open(recipe_file, "w") as f:
f.write(recipe_text)

try:
Recipe(load_file=recipe_file)
except Exception as e:
logger.warning(
"Error parsing recipe with conda-souschef: %s",
repr(e),
exc_info=e,
)
parse_results[parse_name] = False
else:
parse_results[parse_name] = True

parse_name = "conda-recipe-manager"
try:
from conda_recipe_manager.parser.recipe_parser import RecipeParser
except ImportError:
parse_results[parse_name] = None
pass
else:
try:
RecipeParser(recipe_text)
except Exception as e:
logger.warning(
"Error parsing recipe with conda-recipe-manager: %s",
repr(e),
exc_info=e,
)
parse_results[parse_name] = False
else:
parse_results[parse_name] = True

if parse_results:
if any(pv is not None for pv in parse_results.values()):
if not any(parse_results.values()):
lints.append(
"The recipe is not parsable by any of the known "
f"recipe parsers ({sorted(parse_results.keys())}). Please "
"check the logs for more information and ensure your "
"recipe can be parsed."
)
for parser_name, pv in parse_results.items():
if pv is False:
hints.append(
f"The recipe is not parsable by parser `{parser_name}`. Your recipe "
"may not receive automatic updates and/or may not be compatible "
"with conda-forge's infrastructure. Please check the logs for "
"more information and ensure your recipe can be parsed."
)
23 changes: 23 additions & 0 deletions news/2141-recipe-parsing-lint-hint.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Added new ``conda-forge``-only hint+lint for recipe be able to be parsed. (#2141)

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
190 changes: 190 additions & 0 deletions tests/test_lint_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3316,5 +3316,195 @@ def test_hint_noarch_python_use_python_min_v1(
)


def test_lint_recipe_parses_ok():
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, "meta.yaml"), "w") as f:
f.write(
textwrap.dedent(
"""
package:
name: foo
build:
number: 0
test:
imports:
- foo
about:
home: something
license: MIT
license_file: LICENSE
summary: a test recipe
extra:
recipe-maintainers:
- a
- b
"""
)
)
lints, hints = linter.main(tmpdir, return_hints=True, conda_forge=True)
assert not any(
lint.startswith(
"The recipe is not parsable by any of the known recipe parsers"
)
for lint in lints
), lints
assert not any(
hint.startswith("The recipe is not parsable by parser")
for hint in hints
), hints


def test_lint_recipe_parses_forblock():
with tempfile.TemporaryDirectory() as tmpdir:
# CRM cannot parse this one
with open(os.path.join(tmpdir, "meta.yaml"), "w") as f:
f.write(
textwrap.dedent(
"""
package:
name: foo
build:
number: 0
test:
imports:
{% for blah in blahs %}
- {{ blah }}
{% endfor %}
about:
home: something
license: MIT
license_file: LICENSE
summary: a test recipe
extra:
recipe-maintainers:
- a
- b
"""
)
)
lints, hints = linter.main(tmpdir, return_hints=True, conda_forge=True)
assert not any(
lint.startswith(
"The recipe is not parsable by any of the known recipe parsers"
)
for lint in lints
), lints
assert not any(
hint.startswith(
"The recipe is not parsable by parser `conda-forge-tick"
)
for hint in hints
), hints
assert any(
hint.startswith(
"The recipe is not parsable by parser `conda-recipe-manager"
)
for hint in hints
), hints
assert not any(
hint.startswith(
"The recipe is not parsable by parser `conda-souschef"
)
for hint in hints
), hints


def test_lint_recipe_parses_spacing():
with tempfile.TemporaryDirectory() as tmpdir:
# CRM fails if the yaml has differing spacing
with open(os.path.join(tmpdir, "meta.yaml"), "w") as f:
f.write(
textwrap.dedent(
"""
package:
name: foo
build:
number: 0
test:
imports:
- foo
about:
home: something
license: MIT
license_file: LICENSE
summary: a test recipe
extra:
recipe-maintainers:
- a
- b
"""
)
)
lints, hints = linter.main(tmpdir, return_hints=True, conda_forge=True)
assert not any(
lint.startswith(
"The recipe is not parsable by any of the known recipe parsers"
)
for lint in lints
), lints
assert not any(
hint.startswith(
"The recipe is not parsable by parser `conda-forge-tick"
)
for hint in hints
), hints
assert any(
hint.startswith(
"The recipe is not parsable by parser `conda-recipe-manager"
)
for hint in hints
), hints
assert not any(
hint.startswith(
"The recipe is not parsable by parser `conda-souschef"
)
for hint in hints
), hints


def test_lint_recipe_parses_v1_spacing():
with tempfile.TemporaryDirectory() as tmpdir:
with open(os.path.join(tmpdir, "recipe.yaml"), "w") as f:
f.write(
textwrap.dedent(
"""
package:
name: blah
build:
number: ${{ build }}
about:
home: something
license: MIT
license_file: LICENSE
summary: a test recipe
extra:
recipe-maintainers:
- a
- b
"""
)
)
lints, hints = linter.main(tmpdir, return_hints=True, conda_forge=True)
assert any(
lint.startswith(
"The recipe is not parsable by any of the known recipe parsers"
)
for lint in lints
), lints
assert any(
hint.startswith(
"The recipe is not parsable by parser `conda-recipe-manager"
)
for hint in hints
), hints


if __name__ == "__main__":
unittest.main()

0 comments on commit 1b800c2

Please sign in to comment.