Repository: conda-forge/conda-smithy

Issue:
ENH: Allow ignoring `python_min` hints
Some feedstocks need to opt-out of the test pins for `python_min`, for example due to being used elsewhere as a `downstreams:` test against other python versions (c.f. https://github.com/conda-forge/conda-forge.github.io/pull/2387, https://github.com/conda-forge/conda-smithy-feedstock/pull/331, https://github.com/conda-forge/setuptools-feedstock/pull/371).

Given this reality, we shouldn't force those feedstocks to live with linter warnings in perpetuity, but give them a way to silence the linter. In https://github.com/conda-forge/conda-smithy/pull/2090, the following mechanism was introduced
```yaml
linter:
  skip:
    - lint_noarch_selectors
```

This could easily be extended to allow
```yaml
linter:
  skip:
    - hint_python_min
```

Discussion:

Patch:
diff --git a/conda_smithy/data/conda-forge.json b/conda_smithy/data/conda-forge.json
index f7378e2d..9fbca7bd 100644
--- a/conda_smithy/data/conda-forge.json
+++ b/conda_smithy/data/conda-forge.json
@@ -817,7 +817,9 @@
     },
     "Lints": {
       "enum": [
-        "lint_noarch_selectors"
+        "lint_noarch_selectors",
+        "hint_pip_no_build_backend",
+        "hint_python_min"
       ],
       "title": "Lints",
       "type": "string"
diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py
index 82313497..8d633fa6 100644
--- a/conda_smithy/lint_recipe.py
+++ b/conda_smithy/lint_recipe.py
@@ -117,9 +117,9 @@ def lintify_meta_yaml(
     lints = []
     hints = []
     major_sections = list(meta.keys())
-    lints_to_skip = (
-        _get_forge_yaml(recipe_dir).get("linter", {}).get("skip", [])
-    )
+    lints_to_skip = (_get_forge_yaml(recipe_dir).get("linter") or {}).get(
+        "skip"
+    ) or []
 
     # If the recipe_dir exists (no guarantee within this function) , we can
     # find the meta.yaml within it.
@@ -487,6 +487,10 @@ def run_conda_forge_specific(
     hints,
     recipe_version: int = 0,
 ):
+    lints_to_skip = (_get_forge_yaml(recipe_dir).get("linter") or {}).get(
+        "skip"
+    ) or []
+
     # Retrieve sections from meta
     package_section = get_section(
         meta, "package", lints, recipe_version=recipe_version
@@ -589,26 +593,27 @@ def run_conda_forge_specific(
             )
 
     # 8: Ensure the recipe specifies a Python build backend if needed
-    host_or_build_reqs = (requirements_section.get("host") or []) or (
-        requirements_section.get("build") or []
-    )
-    hint_pip_no_build_backend(host_or_build_reqs, recipe_name, hints)
-    for out in outputs_section:
-        if recipe_version == 1:
-            output_requirements = rattler_loader.load_all_requirements(out)
-            build_reqs = output_requirements.get("build") or []
-            host_reqs = output_requirements.get("host") or []
-        else:
-            _req = out.get("requirements") or {}
-            if isinstance(_req, Mapping):
-                build_reqs = _req.get("build") or []
-                host_reqs = _req.get("host") or []
+    if "hint_pip_no_build_backend" not in lints_to_skip:
+        host_or_build_reqs = (requirements_section.get("host") or []) or (
+            requirements_section.get("build") or []
+        )
+        hint_pip_no_build_backend(host_or_build_reqs, recipe_name, hints)
+        for out in outputs_section:
+            if recipe_version == 1:
+                output_requirements = rattler_loader.load_all_requirements(out)
+                build_reqs = output_requirements.get("build") or []
+                host_reqs = output_requirements.get("host") or []
             else:
-                build_reqs = []
-                host_reqs = []
+                _req = out.get("requirements") or {}
+                if isinstance(_req, Mapping):
+                    build_reqs = _req.get("build") or []
+                    host_reqs = _req.get("host") or []
+                else:
+                    build_reqs = []
+                    host_reqs = []
 
-        name = out.get("name", "").strip()
-        hint_pip_no_build_backend(host_reqs or build_reqs, name, hints)
+            name = out.get("name", "").strip()
+            hint_pip_no_build_backend(host_reqs or build_reqs, name, hints)
 
     # 9: No duplicates in conda-forge.yml
     if (
@@ -627,15 +632,16 @@ def run_conda_forge_specific(
             )
 
     # 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,
-        recipe_version,
-        hints,
-    )
+    if "hint_python_min" not in lints_to_skip:
+        hint_noarch_python_use_python_min(
+            requirements_section.get("host") or [],
+            requirements_section.get("run") or [],
+            test_reqs,
+            outputs_section,
+            noarch_value,
+            recipe_version,
+            hints,
+        )
 
     # 11: ensure we can parse the recipe
     if recipe_version == 1:
diff --git a/conda_smithy/schema.py b/conda_smithy/schema.py
index 56687dd2..544daf45 100644
--- a/conda_smithy/schema.py
+++ b/conda_smithy/schema.py
@@ -96,6 +96,8 @@ class BotConfigVersionUpdatesSourcesChoice(StrEnum):
 
 class Lints(StrEnum):
     LINT_NOARCH_SELECTORS = "lint_noarch_selectors"
+    HINT_PIP_NO_BUILD_BACKEND = "hint_pip_no_build_backend"
+    HINT_PYTHON_MIN = "hint_python_min"
 
 
 ##############################################
diff --git a/news/skip-python-hints.rst b/news/skip-python-hints.rst
new file mode 100644
index 00000000..19bf91cb
--- /dev/null
+++ b/news/skip-python-hints.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* Add ``hint_python_min`` and ``hint_pip_no_build_backend`` values to ``linter.skip`` key, allowing to skip respective checks. (#2206)
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* Fix a ``TypeError`` when either the ``linter`` key or the ``skip`` subkey is empty. (#2206)
+
+**Security:**
+
+* <news item>


Test patch:
diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py
index 30901c71..33d93e74 100644
--- a/tests/test_lint_recipe.py
+++ b/tests/test_lint_recipe.py
@@ -3055,6 +3055,7 @@ def test_v1_package_name_version():
         ),
     ],
 )
+@pytest.mark.parametrize("skip", [False, True])
 def test_hint_pip_no_build_backend(
     meta_str,
     expected_hints,
@@ -3062,7 +3063,19 @@ def test_hint_pip_no_build_backend(
     outputs_to_add,
     outputs_expected_hints,
     remove_top_level,
+    skip,
+    tmp_path,
 ):
+    if skip:
+        with open(tmp_path / "conda-forge.yml", "w") as fh:
+            fh.write(
+                """
+linter:
+  skip:
+    - hint_pip_no_build_backend
+"""
+            )
+
     meta = get_yaml().load(meta_str.replace("@@backend@@", backend))
     if remove_top_level:
         meta.pop("requirements", None)
@@ -3076,13 +3089,16 @@ def test_hint_pip_no_build_backend(
             outputs_to_add.replace("@@backend@@", backend)
         )
 
-    total_expected_hints = _expected_hints + outputs_expected_hints
+    if skip:
+        total_expected_hints = []
+    else:
+        total_expected_hints = _expected_hints + outputs_expected_hints
 
     lints = []
     hints = []
     linter.run_conda_forge_specific(
         meta,
-        None,
+        str(tmp_path),
         lints,
         hints,
         recipe_version=0,
@@ -3313,23 +3329,36 @@ def test_hint_pip_no_build_backend(
         ),
     ],
 )
+@pytest.mark.parametrize("skip", [False, True])
 def test_hint_noarch_python_use_python_min(
     meta_str,
     expected_hints,
+    skip,
+    tmp_path,
 ):
+    if skip:
+        with open(tmp_path / "conda-forge.yml", "w") as fh:
+            fh.write(
+                """
+linter:
+  skip:
+    - hint_python_min
+"""
+            )
+
     meta = get_yaml().load(render_meta_yaml(meta_str))
     lints = []
     hints = []
     linter.run_conda_forge_specific(
         meta,
-        None,
+        str(tmp_path),
         lints,
         hints,
         recipe_version=0,
     )
 
     # make sure we have the expected hints
-    if expected_hints:
+    if expected_hints and not skip:
         for expected_hint in expected_hints:
             assert any(expected_hint in hint for hint in hints), hints
     else: