Skip to content

Commit 058670d

Browse files
authored
Merge pull request #2232 from jaimergp/matchspecfields
2 parents 5228b19 + a42dc4c commit 058670d

File tree

4 files changed

+152
-0
lines changed

4 files changed

+152
-0
lines changed

conda_smithy/lint_recipe.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
hint_pip_usage,
3232
hint_shellcheck_usage,
3333
hint_sources_should_not_mention_pypi_io_but_pypi_org,
34+
hint_space_separated_specs,
3435
hint_suggest_noarch,
3536
)
3637
from conda_smithy.linter.lints import (
@@ -410,6 +411,16 @@ def lintify_meta_yaml(
410411
recipe_version=recipe_version,
411412
)
412413

414+
# 7: warn of `name =version=build` specs, suggest `name version build`
415+
# see https://github.com/conda/conda-build/issues/5571#issuecomment-2604505922
416+
if recipe_version == 0:
417+
hint_space_separated_specs(
418+
requirements_section,
419+
test_section,
420+
outputs_section,
421+
hints,
422+
)
423+
413424
return lints, hints
414425

415426

conda_smithy/linter/hints.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,74 @@ def hint_noarch_python_use_python_min(
364364
)
365365
)
366366
hints.append(hint)
367+
368+
369+
def hint_space_separated_specs(
370+
requirements_section,
371+
test_section,
372+
outputs_section,
373+
hints,
374+
):
375+
report = {}
376+
for req_type, reqs in {
377+
**requirements_section,
378+
"test": test_section.get("requires") or (),
379+
}.items():
380+
bad_specs = [
381+
req
382+
for req in (reqs or ())
383+
if not _ensure_spec_space_separated(req)
384+
]
385+
if bad_specs:
386+
report.setdefault("top-level", {})[req_type] = bad_specs
387+
for output in outputs_section:
388+
requirements_section = output.get("requirements") or {}
389+
if not hasattr(requirements_section, "items"):
390+
# not a dict, but a list (CB2 style)
391+
requirements_section = {"run": requirements_section}
392+
for req_type, reqs in {
393+
"build": requirements_section.get("build") or [],
394+
"host": requirements_section.get("host") or [],
395+
"run": requirements_section.get("run") or [],
396+
"test": output.get("test", {}).get("requires") or [],
397+
}.items():
398+
bad_specs = [
399+
req for req in reqs if not _ensure_spec_space_separated(req)
400+
]
401+
if bad_specs:
402+
report.setdefault(output, {})[req_type] = bad_specs
403+
404+
lines = []
405+
for output, requirements in report.items():
406+
lines.append(f"{output} output has some malformed specs:")
407+
for req_type, specs in requirements.items():
408+
specs = [f"`{spec}" for spec in specs]
409+
lines.append(f"- In section {req_type}: {', '.join(specs)}")
410+
if lines:
411+
lines.append(
412+
"Requirements spec fields should always be space-separated to avoid known issues in "
413+
"conda-build. For example, instead of `name =version=build`, use `name version build`."
414+
)
415+
hints.append("\n".join(lines))
416+
417+
418+
def _ensure_spec_space_separated(spec: str) -> bool:
419+
from conda import CondaError
420+
from conda.models.match_spec import MatchSpec
421+
422+
if "#" in spec:
423+
spec = spec.split("#")[0]
424+
spec = spec.strip()
425+
fields = spec.split(" ")
426+
try:
427+
match_spec = MatchSpec(spec)
428+
except CondaError:
429+
return False
430+
431+
if match_spec.strictness == len(fields):
432+
# strictness is a value between 1 and 3:
433+
# 1 = name only
434+
# 2 = name and version
435+
# 3 = name, version and build.
436+
return True
437+
return False

news/2232-matchspecs-equal

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Linter: Report hint if some of the requirements in a ``meta.yaml`` recipe are not using space-separated ``MatchSpec`` syntax. (#2232)
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

tests/test_lint_recipe.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import pytest
1313

1414
import conda_smithy.lint_recipe as linter
15+
from conda_smithy.linter import hints
1516
from conda_smithy.linter.utils import VALID_PYTHON_BUILD_BACKENDS
1617
from conda_smithy.utils import get_yaml, render_meta_yaml
1718

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

42134214

4215+
@pytest.mark.parametrize(
4216+
"spec, result",
4217+
[
4218+
("python", True),
4219+
("python 3.9", True),
4220+
("python 3.9 *cpython*", True),
4221+
("python 3.9=*cpython*", False),
4222+
("python =3.9=*cpython*", False),
4223+
("python=3.9=*cpython*", False),
4224+
("python malformed=*cpython*", False),
4225+
],
4226+
)
4227+
def test_bad_specs(spec, result):
4228+
assert hints._ensure_spec_space_separated(spec) is result
4229+
4230+
4231+
@pytest.mark.parametrize(
4232+
"spec, ok",
4233+
[
4234+
("python", True),
4235+
("python 3.9", True),
4236+
("python 3.9 *cpython*", True),
4237+
("python 3.9=*cpython*", False),
4238+
("python =3.9=*cpython*", False),
4239+
("python=3.9=*cpython*", False),
4240+
("python malformed=*cpython*", False),
4241+
],
4242+
)
4243+
def test_bad_specs_report(tmp_path, spec, ok):
4244+
(tmp_path / "meta.yaml").write_text(
4245+
textwrap.dedent(
4246+
f"""
4247+
package:
4248+
name: foo
4249+
requirements:
4250+
run:
4251+
- {spec}
4252+
"""
4253+
)
4254+
)
4255+
4256+
_, hints = linter.main(tmp_path, return_hints=True)
4257+
print(hints)
4258+
assert all("has some malformed specs" not in hint for hint in hints) is ok
4259+
4260+
42144261
if __name__ == "__main__":
42154262
unittest.main()

0 commit comments

Comments
 (0)