Skip to content

Commit

Permalink
Merge pull request conda-forge#2232 from jaimergp/matchspecfields
Browse files Browse the repository at this point in the history
  • Loading branch information
beckermr authored Jan 27, 2025
2 parents 5228b19 + a42dc4c commit 058670d
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 0 deletions.
11 changes: 11 additions & 0 deletions conda_smithy/lint_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
hint_pip_usage,
hint_shellcheck_usage,
hint_sources_should_not_mention_pypi_io_but_pypi_org,
hint_space_separated_specs,
hint_suggest_noarch,
)
from conda_smithy.linter.lints import (
Expand Down Expand Up @@ -410,6 +411,16 @@ def lintify_meta_yaml(
recipe_version=recipe_version,
)

# 7: warn of `name =version=build` specs, suggest `name version build`
# see https://github.com/conda/conda-build/issues/5571#issuecomment-2604505922
if recipe_version == 0:
hint_space_separated_specs(
requirements_section,
test_section,
outputs_section,
hints,
)

return lints, hints


Expand Down
71 changes: 71 additions & 0 deletions conda_smithy/linter/hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,74 @@ def hint_noarch_python_use_python_min(
)
)
hints.append(hint)


def hint_space_separated_specs(
requirements_section,
test_section,
outputs_section,
hints,
):
report = {}
for req_type, reqs in {
**requirements_section,
"test": test_section.get("requires") or (),
}.items():
bad_specs = [
req
for req in (reqs or ())
if not _ensure_spec_space_separated(req)
]
if bad_specs:
report.setdefault("top-level", {})[req_type] = bad_specs
for output in outputs_section:
requirements_section = output.get("requirements") or {}
if not hasattr(requirements_section, "items"):
# not a dict, but a list (CB2 style)
requirements_section = {"run": requirements_section}
for req_type, reqs in {
"build": requirements_section.get("build") or [],
"host": requirements_section.get("host") or [],
"run": requirements_section.get("run") or [],
"test": output.get("test", {}).get("requires") or [],
}.items():
bad_specs = [
req for req in reqs if not _ensure_spec_space_separated(req)
]
if bad_specs:
report.setdefault(output, {})[req_type] = bad_specs

lines = []
for output, requirements in report.items():
lines.append(f"{output} output has some malformed specs:")
for req_type, specs in requirements.items():
specs = [f"`{spec}" for spec in specs]
lines.append(f"- In section {req_type}: {', '.join(specs)}")
if lines:
lines.append(
"Requirements spec fields should always be space-separated to avoid known issues in "
"conda-build. For example, instead of `name =version=build`, use `name version build`."
)
hints.append("\n".join(lines))


def _ensure_spec_space_separated(spec: str) -> bool:
from conda import CondaError
from conda.models.match_spec import MatchSpec

if "#" in spec:
spec = spec.split("#")[0]
spec = spec.strip()
fields = spec.split(" ")
try:
match_spec = MatchSpec(spec)
except CondaError:
return False

if match_spec.strictness == len(fields):
# strictness is a value between 1 and 3:
# 1 = name only
# 2 = name and version
# 3 = name, version and build.
return True
return False
23 changes: 23 additions & 0 deletions news/2232-matchspecs-equal
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Linter: Report hint if some of the requirements in a ``meta.yaml`` recipe are not using space-separated ``MatchSpec`` syntax. (#2232)

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
47 changes: 47 additions & 0 deletions tests/test_lint_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import pytest

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.utils import get_yaml, render_meta_yaml

Expand Down Expand Up @@ -4211,5 +4212,51 @@ def test_version_zero(filename: str):
assert "Package version is missing." not in lints


@pytest.mark.parametrize(
"spec, result",
[
("python", True),
("python 3.9", True),
("python 3.9 *cpython*", True),
("python 3.9=*cpython*", False),
("python =3.9=*cpython*", False),
("python=3.9=*cpython*", False),
("python malformed=*cpython*", False),
],
)
def test_bad_specs(spec, result):
assert hints._ensure_spec_space_separated(spec) is result


@pytest.mark.parametrize(
"spec, ok",
[
("python", True),
("python 3.9", True),
("python 3.9 *cpython*", True),
("python 3.9=*cpython*", False),
("python =3.9=*cpython*", False),
("python=3.9=*cpython*", False),
("python malformed=*cpython*", False),
],
)
def test_bad_specs_report(tmp_path, spec, ok):
(tmp_path / "meta.yaml").write_text(
textwrap.dedent(
f"""
package:
name: foo
requirements:
run:
- {spec}
"""
)
)

_, hints = linter.main(tmp_path, return_hints=True)
print(hints)
assert all("has some malformed specs" not in hint for hint in hints) is ok


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

0 comments on commit 058670d

Please sign in to comment.