Repository: conda-incubator/grayskull (python)

Issue:
[BUG] requirements needed with outputs
Conda forge fails with the message `'PYTHON' is undefined` when the `outputs` section is defined (i.e. extra requires).

The reason is this

```yaml
outputs:
  - name: pypushflow
    requirements:
      host: &id001
        - python >=3.6
        - pip
      run:
        - python >=3.6
  - name: pypushflow-mx
    requirements:
      host: *id001
      run:
        - pypushflow =={{ version }}
        - pymongo >=4
```

which should become

```yaml
requirements: # !!!!
  host:
    - python >=3.6
    - pip

outputs:
  - name: pypushflow
    requirements:
      host:
        - python >=3.6
        - pip
      run:
        - python >=3.6
  - name: pypushflow-mx
    requirements:
      host:
        - python >=3.6
        - pip
      run:
        - python >=3.6  # !!!!!
        - pypushflow =={{ version }}
        - pymongo >=4
```

Command:

```
grayskull pypi pypushflow --extras-require-all --extras-require-split --extras-require-test test --extras-require-exclude dev --strict-conda-forge
```

Pull request description:
Fix issue 382
Closes #382 

Patch:
diff --git a/grayskull/strategy/pypi.py b/grayskull/strategy/pypi.py
index 549953b..f9a51fd 100644
--- a/grayskull/strategy/pypi.py
+++ b/grayskull/strategy/pypi.py
@@ -408,15 +408,24 @@ def get_metadata(recipe, config) -> dict:
     outputs = []
     if optional_requirements:
         if config.extras_require_split:
+            output_req_section = dict(requirements_section)
+            requirements_section = {
+                k: v for k, v in requirements_section.items() if k == "host"
+            }
             outputs.append(
                 {
                     "name": name,
-                    "requirements": requirements_section,
+                    "requirements": dict(output_req_section),
                 }
             )
+            output_req_section["run"] = [
+                s for s in output_req_section["run"] if "python" in s
+            ]
             for option, req_list in optional_requirements.items():
-                req_section = dict(requirements_section)
-                req_section["run"] = [f"{name} =={{{{ version }}}}"] + req_list
+                req_section = dict(output_req_section)
+                req_section["run"] = list(req_section.get("run", list()))
+                req_section["run"].append(f"{name} =={{{{ version }}}}")
+                req_section["run"].extend(req_list)
                 outputs.append(
                     {
                         "name": f"{name}-{option}",
@@ -446,6 +455,7 @@ def get_metadata(recipe, config) -> dict:
         return {
             "package": package_section,
             "build": build_section,
+            "requirements": requirements_section,
             "outputs": outputs,
             "test": test_section,
             "about": about_section,
@@ -491,6 +501,11 @@ def update_recipe(recipe: Recipe, config: Configuration, all_sections: List[str]
 
     if not config.is_arch:
         recipe["build"]["noarch"] = "python"
+        if "outputs" in recipe:
+            for output in recipe["outputs"]:
+                if "build" not in output:
+                    output["build"] = dict()
+                output["build"]["noarch"] = "python"
 
 
 def extract_requirements(metadata: dict, config, recipe) -> Dict[str, List[str]]:


Test patch:
diff --git a/tests/test_pypi.py b/tests/test_pypi.py
index 020eaef..daaaae3 100644
--- a/tests/test_pypi.py
+++ b/tests/test_pypi.py
@@ -289,6 +289,7 @@ def test_get_include_extra_requirements():
         "pyyaml >=5.3.1",
         "toolz >=0.8.2",
     ]
+    host_requirements = ["python >=3.8", "pip"]
 
     extras = dict()
     extras["array"] = ["numpy >=1.18"]
@@ -311,7 +312,8 @@ def test_get_include_extra_requirements():
     config = Configuration(name="dask", version="2022.6.1")
     recipe = GrayskullFactory.create_recipe("pypi", config)
     recipe["name"] == "dask"
-    assert "outputs" not in recipe
+    assert set(recipe["outputs"]) == set()
+    assert set(recipe["requirements"]["host"]) == set(host_requirements)
     assert set(recipe["requirements"]["run"]) == set(base_requirements)
     assert set(recipe["test"]["requires"]) == {"pip"}
 
@@ -319,13 +321,14 @@ def test_get_include_extra_requirements():
     config = Configuration(name="dask", version="2022.6.1", extras_require_all=True)
     recipe = GrayskullFactory.create_recipe("pypi", config)
     recipe["name"] == "dask"
-    assert "outputs" not in recipe
+    assert set(recipe["outputs"]) == set()
 
     expected = list(base_requirements)
     for name, req_lst in extras.items():
         if name != "complete":
             expected.append(f"Extra: {name}")
             expected.extend(req_lst)
+    assert set(recipe["requirements"]["host"]) == set(host_requirements)
     assert set_of_strings(recipe["requirements"]["run"]) == set(expected)
     assert set_of_strings(recipe["test"]["requires"]) == {"pip"}
 
@@ -339,13 +342,14 @@ def test_get_include_extra_requirements():
     )
     recipe = GrayskullFactory.create_recipe("pypi", config)
     recipe["name"] == "dask"
-    assert "outputs" not in recipe
+    assert set(recipe["outputs"]) == set()
 
     expected = list(base_requirements)
     for name, req_lst in extras.items():
         if name not in ("test", "complete"):
             expected.append(f"Extra: {name}")
             expected.extend(req_lst)
+    assert set(recipe["requirements"]["host"]) == set(host_requirements)
     assert set_of_strings(recipe["requirements"]["run"]) == set(expected)
     assert set_of_strings(recipe["test"]["requires"]) == {"pip", *extras["test"]}
 
@@ -355,7 +359,8 @@ def test_get_include_extra_requirements():
     )
     recipe = GrayskullFactory.create_recipe("pypi", config)
     recipe["name"] == "dask"
-    assert "outputs" not in recipe
+    assert set(recipe["outputs"]) == set()
+    assert set(recipe["requirements"]["host"]) == set(host_requirements)
     assert set_of_strings(recipe["requirements"]["run"]) == {
         *base_requirements,
         "Extra: array",
@@ -373,7 +378,8 @@ def test_get_include_extra_requirements():
     )
     recipe = GrayskullFactory.create_recipe("pypi", config)
     recipe["name"] == "dask"
-    assert "outputs" not in recipe
+    assert set(recipe["outputs"]) == set()
+    assert set(recipe["requirements"]["host"]) == set(host_requirements)
     assert set_of_strings(recipe["requirements"]["run"]) == set(base_requirements)
     assert set_of_strings(recipe["test"]["requires"]) == {"pip", *extras["test"]}
 
@@ -388,7 +394,8 @@ def test_get_include_extra_requirements():
     )
     recipe = GrayskullFactory.create_recipe("pypi", config)
     recipe["name"] == "dask"
-    assert "outputs" not in recipe
+    assert set(recipe["outputs"]) == set()
+    assert set(recipe["requirements"]["host"]) == set(host_requirements)
     assert set_of_strings(recipe["requirements"]["run"]) == set(base_requirements)
     assert set_of_strings(recipe["test"]["requires"]) == {"pip", *extras["test"]}
 
@@ -403,14 +410,19 @@ def test_get_include_extra_requirements():
     )
     recipe = GrayskullFactory.create_recipe("pypi", config)
     recipe["name"] == "dask-meta"
-    assert not list(recipe["requirements"].items())
+    assert set(recipe["requirements"]["host"]) == set(host_requirements)
+    assert not recipe["requirements"]["run"]
     assert len(recipe["outputs"]) == 6
 
     expected = dict()
     expected["dask"] = set(base_requirements)
     for name, req_lst in extras.items():
         if name != "test":
-            expected[f"dask-{name}"] = {"dask =={{ version }}", *req_lst}
+            expected[f"dask-{name}"] = {
+                "dask =={{ version }}",
+                "python >=3.8",
+                *req_lst,
+            }
     found = dict()
     for output in recipe["outputs"]:
         found[output["name"]] = set_of_strings(output["requirements"]["run"])
@@ -1226,6 +1238,7 @@ def test_section_order():
         "build",
         "requirements",
         "test",
+        "outputs",
         "about",
         "extra",
     ) == tuple(recipe.keys())