Repository: conda-forge/conda-smithy
Pull request #2281: Update matchspec hint for `noarch: python` for v1 recipes
Author: login: iamthebot
Created: 2025-03-20T16:22:24Z | Merged: 2025-03-21T11:45:12Z

Closes conda-forge/staged-recipes#29468

tl;dr The hint suggests using `python ${{ python_min }}` in the host and `test.requires` sections; however, rattler-build no longer accepts that as a valid matchspec because it's ambiguous.

Instead, users should use `python ${{ python_min }}.*` in these sections.

Tested against a recipe using the ambiguous matchspec and also verified it doesn't trigger if `python ${{ python_min }}.*` is used.

<!--
Thank you for pull request.
Below are a few things we ask you kindly to self-check before getting a review. Remove checks that are not relevant.
-->
Checklist
* [x] Added a ``news`` entry
* [ ] Regenerated schema JSON if schema altered (`python conda_smithy/schema.py`)

<!--
Please note any issues this fixes using [closing keywords]( https://help.github.com/articles/closing-issues-using-keywords/ ):
-->

<!--
Please add any other relevant info below:
-->



Diff:
diff --git a/conda_smithy/linter/hints.py b/conda_smithy/linter/hints.py
index a23f7efb1..fc2c8372b 100644
--- a/conda_smithy/linter/hints.py
+++ b/conda_smithy/linter/hints.py
@@ -279,12 +279,19 @@ def _hint_noarch_python_use_python_min_inner(
             ),
         ]:
             if recipe_version == 1:
+                # V1 recipes now require a `python ${{ python_min }}.*` matchspec
+                # in lieu of the ambiguous `python {{ python_min }}` matchspec
                 syntax = syntax.replace(
                     "{{ python_min }}", r"\${{ python_min }}"
                 )
-                report_syntax = report_syntax.replace(
-                    "{{ python_min }}", "${{ python_min }}"
-                )
+                if section_name in ["host", "test.requires"]:
+                    report_syntax = report_syntax.replace(
+                        "{{ python_min }}", "${{ python_min }}.*"
+                    )
+                else:
+                    report_syntax = report_syntax.replace(
+                        "{{ python_min }}", "${{ python_min }}"
+                    )
                 test_syntax = syntax
             else:
                 test_syntax = syntax.replace("{{ python_min }}", "9999")
diff --git a/news/2281-v1-pythonmin-hint.rst b/news/2281-v1-pythonmin-hint.rst
new file mode 100644
index 000000000..f1f80d45e
--- /dev/null
+++ b/news/2281-v1-pythonmin-hint.rst
@@ -0,0 +1,2 @@
+**Changed:**
+* In V1 recipes, the hint for ``python_min`` usage in the host and test.requires section now uses ``python {{ python_min }}.*`` for compatibility with the latest versions of rattler-build used in conda-forge feedstocks. (#2281)
diff --git a/tests/conftest.py b/tests/conftest.py
index 25d56ca08..ce8bdbac5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -167,9 +167,11 @@ def noarch_recipe(config_yaml: ConfigYAML, recipe_dirname):
 @pytest.fixture(scope="function")
 def noarch_recipe_with_python_min(config_yaml: ConfigYAML, recipe_dirname):
     if config_yaml.type == "rattler-build":
-        jinjatxt = "${{ python_min }}"
+        jinjatxt_run = "${{ python_min }}"
+        jinjatxt_host_test = "${{ python_min }}.*"
     else:
-        jinjatxt = "{{ python_min }}"
+        jinjatxt_run = "{{ python_min }}"
+        jinjatxt_host_test = jinjatxt_run
     with open(
         os.path.join(
             config_yaml.workdir, recipe_dirname, config_yaml.recipe_name
@@ -185,9 +187,9 @@ def noarch_recipe_with_python_min(config_yaml: ConfigYAML, recipe_dirname):
     noarch: python
 requirements:
     host:
-        - python {jinjatxt}
+        - python {jinjatxt_host_test}
     run:
-        - python >={jinjatxt}
+        - python >={jinjatxt_run}
     """
         )
     return RecipeConfigPair(
diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py
index a81cba69b..0b6106b75 100644
--- a/tests/test_lint_recipe.py
+++ b/tests/test_lint_recipe.py
@@ -163,7 +163,6 @@ def test_osx_lint(where):
 
 
 def test_stdlib_lints_multi_output():
-
     with tmp_directory() as recipe_dir:
         with open(os.path.join(recipe_dir, "meta.yaml"), "w") as fh:
             fh.write(
@@ -666,10 +665,7 @@ def test_maintainers_section(self):
         )
         self.assertNotIn(expected_message, lints)
 
-        expected_message = (
-            'The "extra" section was expected to be a '
-            "dictionary, but got a list."
-        )
+        expected_message = 'The "extra" section was expected to be a dictionary, but got a list.'
         lints, hints = linter.lintify_meta_yaml(
             {"extra": ["recipe-maintainers"]}
         )
@@ -1050,10 +1046,7 @@ def assert_noarch_selector(
                         f"been a lint for '{meta_string}'."
                     )
                 else:
-                    message = (
-                        f"Expected lints for '{meta_string}', but didn't "
-                        "get any."
-                    )
+                    message = f"Expected lints for '{meta_string}', but didn't get any."
                 self.assertEqual(
                     not is_good,
                     any(lint.startswith(expected_start) for lint in lints),
@@ -1250,10 +1243,7 @@ def assert_noarch_selector(
                         f"been a lint for '{meta_string}'."
                     )
                 else:
-                    message = (
-                        f"Expected lints for '{meta_string}', but didn't "
-                        "get any."
-                    )
+                    message = f"Expected lints for '{meta_string}', but didn't get any."
                 self.assertEqual(
                     not is_good,
                     any(lint.startswith(expected_start) for lint in lints),
@@ -1355,10 +1345,7 @@ def assert_noarch_hint(meta_string, is_good=False):
                         f"been a lint for '{meta_string}'."
                     )
                 else:
-                    message = (
-                        f"Expected hints for '{meta_string}', but didn't "
-                        "get any."
-                    )
+                    message = f"Expected hints for '{meta_string}', but didn't get any."
                 self.assertEqual(
                     not is_good,
                     any(lint.startswith(expected_start) for lint in hints),
@@ -1436,10 +1423,7 @@ def assert_noarch_hint(meta_string, is_good=False):
                         f"been a hint for '{meta_string}'."
                     )
                 else:
-                    message = (
-                        f"Expected hints for '{meta_string}', but didn't "
-                        "get any."
-                    )
+                    message = f"Expected hints for '{meta_string}', but didn't get any."
                 self.assertEqual(
                     not is_good,
                     any(lint.startswith(expected_start) for lint in hints),
@@ -1739,7 +1723,7 @@ def test_redundant_license(self):
         }
         lints, hints = linter.lintify_meta_yaml(meta)
         expected_message = (
-            "The recipe `license` should not include " 'the word "License".'
+            'The recipe `license` should not include the word "License".'
         )
         self.assertIn(expected_message, lints)
 
@@ -3629,7 +3613,7 @@ def test_hint_noarch_python_use_python_min(
                 """
             ),
             [
-                "python ${{ python_min }}",
+                "python ${{ python_min }}.*",
                 "python >=${{ python_min }}",
             ],
         ),
@@ -3644,14 +3628,14 @@ def test_hint_noarch_python_use_python_min(
 
                 requirements:
                   host:
-                    - python ${{ python_min }}
+                    - python ${{ python_min }}.*
                   run:
                     - python >=${{ python_min }}
 
                 tests:
                   - requirements:
                       run:
-                        - python ${{ python_min }}
+                        - python ${{ python_min }}.*
                 """
             ),
             [],
@@ -3687,7 +3671,7 @@ def test_hint_noarch_python_use_python_min(
                 """
             ),
             [
-                "python ${{ python_min }}",
+                "python ${{ python_min }}.*",
                 "python >=${{ python_min }}",
             ],
         ),
@@ -3704,14 +3688,14 @@ def test_hint_noarch_python_use_python_min(
                   host:
                     - if: blah
                       then: blahblah
-                      else: python ${{ python_min }}
+                      else: python ${{ python_min }}.*
                   run:
                     - python >=${{ python_min }}
 
                 tests:
                   - requirements:
                       run:
-                        - python ${{ python_min }}
+                        - python ${{ python_min }}.*
                 """
             ),
             [],
@@ -3792,7 +3776,7 @@ def test_hint_noarch_python_use_python_min(
       - plip --help
     requirements:
       run:
-        - python ${{ python_min }}
+        - python ${{ python_min }}.*
 """,
             [],
         ),
@@ -3811,14 +3795,14 @@ def test_hint_noarch_python_use_python_min(
                       host:
                         - if: blah
                           then: blahblah
-                          else: python ${{ python_min }}
+                          else: python ${{ python_min }}.*
                       run:
                         - python >=${{ python_min }}
 
                     tests:
                       - requirements:
                           run:
-                            - python ${{ python_min }}
+                            - python ${{ python_min }}.*
                 """
             ),
             [],
@@ -3845,7 +3829,7 @@ def test_hint_noarch_python_use_python_min(
                     tests:
                       - requirements:
                           run:
-                            - python ${{ python_min }}
+                            - python ${{ python_min }}.*
                   - package:
                       name: python-bar
                     build:
@@ -3854,14 +3838,14 @@ def test_hint_noarch_python_use_python_min(
                       host:
                         - if: blah
                           then: blahblah
-                          else: python ${{ python_min }}
+                          else: python ${{ python_min }}.*
                       run:
                         - python
 
                     tests:
                       - requirements:
                           run:
-                            - python ${{ python_min }}
+                            - python ${{ python_min }}.*
                 """
             ),
             [
@@ -3884,14 +3868,14 @@ def test_hint_noarch_python_use_python_min(
                       host:
                         - if: blah
                           then: blahblah
-                          else: python ${{ python_min }}
+                          else: python ${{ python_min }}.*
                       run:
                         - python >=${{ python_min }}
 
                     tests:
                       - requirements:
                           run:
-                            - python ${{ python_min }}
+                            - python ${{ python_min }}.*
                   - package:
                       name: python-bar
                     requirements:
@@ -3962,7 +3946,7 @@ def test_hint_noarch_python_from_main_v1():
 
 requirements:
   host:
-    - python ${{ python_min }}
+    - python ${{ python_min }}.*
     - pip
     - setuptools
   run:
@@ -3984,7 +3968,7 @@ def test_hint_noarch_python_from_main_v1():
       - plip --help
     requirements:
       run:
-        - python ${{ python_min }}
+        - python ${{ python_min }}.*
 
 about:
   license: GPL-2.0-only
@@ -4460,7 +4444,10 @@ def test_find_recipe_directory(
 
     assert linter.find_recipe_directory(
         str(tmp_path / directory_param), None
-    ) == (str(tmp_path / directory_param), expected_tool)
+    ) == (
+        str(tmp_path / directory_param),
+        expected_tool,
+    )
 
 
 @pytest.mark.parametrize(
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 0d8111d44..ab540ad2a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -41,7 +41,7 @@ def test_get_metadata_from_feedstock_dir_jinja2(noarch_recipe_with_python_min):
 
     if build_tool == RATTLER_BUILD:
         assert metadata.meta["requirements"]["host"] == [
-            "python ${{ python_min }}"
+            "python ${{ python_min }}.*"
         ]
     else:
         assert metadata.meta["requirements"]["host"] == ["python 2.7"]


Reviews:
user: login: beckermr
body: Needs a news item.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-20T16:31:44Z
user: login: beckermr
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T22:04:52Z
user: login: beckermr
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T22:05:20Z
user: login: beckermr
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T11:42:19Z
user: login: beckermr
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T11:42:53Z

Review comments:
diff_hunk: @@ -3932,6 +3916,9 @@ def test_hint_noarch_python_use_python_min_v1(
         for expected_hint in expected_hints:
             assert any(expected_hint in hint for hint in hints), hints
     else:
+        for hint in hints:
+            if "python_min" in hint:
+                print(hint)
path: tests/test_lint_recipe.py
user: login: beckermr
body: We can remove this bit.
created_at: 2025-03-20T22:04:52Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,2 @@
+**Changed:**
+* In V1 recipes, the hint for `python_min` usage in the host and test.requires section now uses `python {{ python_min }}.*` for compatibility with the latest versions of rattler-build used in conda-forge feedstocks. (#2281)
path: news/2281-v1-pythonmin-hint.rst
user: login: beckermr
body: ```suggestion
* In V1 recipes, the hint for ``python_min`` usage in the host and test.requires section now uses ``python {{ python_min }}.*`` for compatibility with the latest versions of rattler-build used in conda-forge feedstocks. (#2281)
```
created_at: 2025-03-20T22:05:20Z
author_association: MEMBER
diff_hunk: @@ -3932,6 +3916,9 @@ def test_hint_noarch_python_use_python_min_v1(
         for expected_hint in expected_hints:
             assert any(expected_hint in hint for hint in hints), hints
     else:
+        for hint in hints:
+            if "python_min" in hint:
+                print(hint)
path: tests/test_lint_recipe.py
user: login: beckermr
body: ```suggestion
```
created_at: 2025-03-21T11:42:19Z
author_association: MEMBER

PR comments:
user: login: iamthebot
created_at: 2025-03-20T16:33:59Z
body: > Needs a news item.

Also need to fix some tests it turns out
author_association: CONTRIBUTOR
user: login: iamthebot
created_at: 2025-03-20T20:32:33Z
body: > Needs a news item.

@beckermr sorry about that, tests are fixed + news item added. Ready for review!
author_association: CONTRIBUTOR
user: login: beckermr
created_at: 2025-03-20T22:05:51Z
body: Just a few small comments. Thanks!
author_association: MEMBER