diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py index 047872062..a443d6b4d 100644 --- a/conda_smithy/lint_recipe.py +++ b/conda_smithy/lint_recipe.py @@ -26,6 +26,7 @@ from conda_smithy.linter import conda_recipe_v1_linter from conda_smithy.linter.hints import ( hint_check_spdx, + hint_noarch_python_use_python_min, hint_pip_no_build_backend, hint_pip_usage, hint_shellcheck_usage, @@ -469,6 +470,20 @@ def run_conda_forge_specific( meta, "outputs", lints, recipe_version=recipe_version ) + build_section = get_section(meta, "build", lints, recipe_version) + noarch_value = build_section.get("noarch") + + if recipe_version == 1: + test_section = get_section(meta, "tests", lints, recipe_version) + test_reqs = [] + for test_element in test_section: + test_reqs += (test_element.get("requirements") or {}).get( + "run" + ) or [] + else: + test_section = get_section(meta, "test", lints, recipe_version) + test_reqs = test_section.get("requires") or [] + # Fetch list of recipe maintainers maintainers = extra_section.get("recipe-maintainers", []) @@ -580,6 +595,16 @@ def run_conda_forge_specific( "The ``conda-forge.yml`` file is not allowed to have duplicate keys." ) + # 10: check for proper noarch python syntax + hint_noarch_python_use_python_min( + requirements_section.get("host") or [], + requirements_section.get("run") or [], + test_reqs, + outputs_section, + noarch_value, + hints, + ) + def _format_validation_msg(error: jsonschema.ValidationError): """Use the data on the validation error to generate improved reporting. diff --git a/conda_smithy/linter/hints.py b/conda_smithy/linter/hints.py index d9f255c6b..86e53475b 100644 --- a/conda_smithy/linter/hints.py +++ b/conda_smithy/linter/hints.py @@ -232,3 +232,29 @@ def hint_pip_no_build_backend(host_or_build_section, package_name, hints): "If your recipe has built with only `pip` in the `host` section in the past, you likely should " "add `setuptools` to the `host` section of your recipe." ) + + +def hint_noarch_python_use_python_min( + host_reqs, run_reqs, test_reqs, outputs_section, noarch_value, hints +): + if noarch_value == "python" and not outputs_section: + for section_name, syntax, reqs in [ + ("host", "python {{ python_min }}.*", host_reqs), + ("run", "python >={{ python_min }}", run_reqs), + ("test.requires", "python ={{ python_min }}", test_reqs), + ]: + for req in reqs: + if ( + req.strip().split()[0] == "python" + and req != "python" + and syntax in req + ): + break + else: + hints.append( + f"noarch: python recipes should almost always follow the syntax in " + f"our [documentation](https://conda-forge.org/docs/maintainer/knowledge_base/#noarch-python). " + f"For the `{section_name}` section of the recipe, you should almost always use `{syntax}` " + f"for the `python` entry. You may need to override the `python_min` variable if the package " + f"requires a newer Python version than the currently supported minimum version on `conda-forge`." + ) diff --git a/news/2115-noarch-python-hints.rst b/news/2115-noarch-python-hints.rst new file mode 100644 index 000000000..2104a7ba8 --- /dev/null +++ b/news/2115-noarch-python-hints.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added hints for new ``noarch: python`` syntax for CFEP-25. (#2115) + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py index f7e35885d..07ca3772a 100644 --- a/tests/test_lint_recipe.py +++ b/tests/test_lint_recipe.py @@ -3002,5 +3002,193 @@ def test_hint_pip_no_build_backend( ), lints +@pytest.mark.parametrize( + "meta_str,expected_hints", + [ + ( + textwrap.dedent( + """ + package: + name: python + + requirements: + run: + - python + """ + ), + [], + ), + ( + textwrap.dedent( + """ + package: + name: python + + build: + noarch: python + + requirements: + run: + - python + """ + ), + [ + "python {{ python_min }}.*", + "python >={{ python_min }}", + "python ={{ python_min }}", + ], + ), + ( + textwrap.dedent( + """ + package: + name: python + + build: + noarch: python + + requirements: + host: + - python + """ + ), + [ + "python {{ python_min }}.*", + "python >={{ python_min }}", + "python ={{ python_min }}", + ], + ), + ( + textwrap.dedent( + """ + package: + name: python + + build: + noarch: python + + test: + requires: + - python + """ + ), + [ + "python {{ python_min }}.*", + "python >={{ python_min }}", + "python ={{ python_min }}", + ], + ), + ( + textwrap.dedent( + """ + package: + name: python + + build: + noarch: python + + requirements: + run: + - python >={{ python_min }} + """ + ), + [ + "python {{ python_min }}.*", + "python ={{ python_min }}", + ], + ), + ( + textwrap.dedent( + """ + package: + name: python + + build: + noarch: python + + requirements: + host: + - python {{ python_min }}.* + run: + - python >={{ python_min }} + """ + ), + [ + "python ={{ python_min }}", + ], + ), + ( + textwrap.dedent( + """ + package: + name: python + + build: + noarch: python + + requirements: + host: + - python {{ python_min }}.* + run: + - python >={{ python_min }} + + test: + requires: + - python ={{ python_min }} + """ + ), + [], + ), + ( + textwrap.dedent( + """ + package: + name: python + + build: + noarch: python + + requirements: + host: + - python {{ python_min }}.* + run: + - python + + test: + requires: + - python ={{ python_min }} + """ + ), + ["python >={{ python_min }}"], + ), + ], +) +def test_hint_noarch_python_use_python_min( + meta_str, + expected_hints, +): + meta = get_yaml().load(meta_str) + lints = [] + hints = [] + linter.run_conda_forge_specific( + meta, + None, + lints, + hints, + recipe_version=0, + ) + + # make sure we have the expected hints + if expected_hints: + for expected_hint in expected_hints: + assert any(expected_hint in hint for hint in hints), hints + else: + assert all( + "noarch: python recipes should almost always follow the syntax in" + not in hint + for hint in hints + ) + + if __name__ == "__main__": unittest.main()