Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions grayskull/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ class Configuration:
PyVer(3, 10),
PyVer(3, 11),
PyVer(3, 12),
PyVer(3, 13),
]
)
py_cf_supported: list[PyVer] = field(
default_factory=lambda: [
PyVer(3, 7),
PyVer(3, 8),
PyVer(3, 9),
PyVer(3, 10),
PyVer(3, 11),
PyVer(3, 12),
PyVer(3, 13),
]
)
is_strict_cf: bool = False
Expand Down
2 changes: 2 additions & 0 deletions grayskull/strategy/py_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,8 @@ def split_deps(deps: str) -> list[str]:
for val in re.split(r"([><!=~^]+)", d):
if not val:
continue
# fix {{var}} back to {{ var }} (broken by split/join)
val = val.replace("{{", "{{ ").replace("}}", " }}")
if {">", "<", "=", "!", "~", "^"} & set(val):
constrain = val.strip()
else:
Expand Down
52 changes: 46 additions & 6 deletions grayskull/strategy/pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,11 @@ def get_metadata(recipe, config) -> dict:
test_requirements = optional_requirements.pop(config.extras_require_test, [])
test_section = compose_test_section(metadata, test_requirements)

if config.is_strict_cf and not config.is_arch:
test_section["requires"] = set_python_min(
test_section["requires"], "test", recipe
)

about_section = {
"home": compute_home(metadata),
"summary": metadata.get("summary"),
Expand Down Expand Up @@ -633,6 +638,36 @@ def check_noarch_python_for_new_deps(
config.is_arch = False


def set_python_min(req_list: list, section: str, recipe) -> list:
if not req_list:
return req_list
python_min = "{{ python_min }}"
map_section = {
"host": f"{python_min}.*",
"run": f">={python_min}",
"test": f"{python_min}.*",
}

# see if there's a single lower bound right now
# TODO: do we need to account for different python deps across dependency types?
python_req_re = re.compile(r"python\s*>=(\d+\.\d+)", re.IGNORECASE)
python_min_req = set(
dep.strip() for dep in req_list if python_req_re.fullmatch(dep)
)

python_match = "python"
if len(python_min_req) == 1:
python_match = python_min_req.pop()
set_global_jinja_var(
recipe, "python_min", python_req_re.fullmatch(python_match).group(1)
)

return [
f"python {map_section[section]}" if dep.lower().strip() == python_match else dep
for dep in req_list
]


def extract_requirements(metadata: dict, config, recipe) -> dict[str, list[str]]:
"""Extract the requirements for `build`, `host` and `run`"""
name = metadata["name"]
Expand All @@ -643,13 +678,13 @@ def extract_requirements(metadata: dict, config, recipe) -> dict[str, list[str]]
build_req = format_dependencies(build_requires or [], config.name)
if not requires_dist and not host_req and not metadata.get("requires_python"):
if config.is_strict_cf:
py_constrain = (
f" >={config.py_cf_supported[0].major}"
f".{config.py_cf_supported[0].minor}"
)
requirements = {
"host": ["python", "pip"],
"run": ["python"],
}
return {
"host": [f"python {py_constrain}", "pip"],
"run": [f"python {py_constrain}"],
"host": set_python_min(requirements["host"], "host", recipe),
"run": set_python_min(requirements["run"], "run", recipe),
}
else:
return {"host": ["python", "pip"], "run": ["python"]}
Expand Down Expand Up @@ -699,6 +734,9 @@ def extract_requirements(metadata: dict, config, recipe) -> dict[str, list[str]]
if metadata.get("requirements_run_constrained", None):
result.update({"run_constrained": metadata["requirements_run_constrained"]})
update_requirements_with_pin(result)
if config.is_strict_cf and not config.is_arch:
result["host"] = set_python_min(result["host"], "host", recipe)
result["run"] = set_python_min(result["run"], "run", recipe)
return result


Expand Down Expand Up @@ -768,6 +806,8 @@ def normalize_requirements_list(requirements: list[str], config) -> list[str]:
def compose_test_section(metadata: dict, test_requirements: list[str]) -> dict:
test_imports = get_test_imports(metadata, metadata["name"])
test_requirements = ["pip"] + test_requirements
if "python" not in test_requirements:
test_requirements.append("python")
test_commands = ["pip check"]
if any("pytest" in req for req in test_requirements):
test_commands.extend(f"pytest --pyargs {module}" for module in test_imports)
Expand Down
1 change: 1 addition & 0 deletions tests/data/poetry/langchain-expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ test:
- langchain-server --help
requires:
- pip
- python

about:
home: https://www.github.com/hwchase17/langchain
Expand Down
109 changes: 85 additions & 24 deletions tests/test_pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
normalize_requirements_list,
remove_all_inner_nones,
remove_selectors_pkgs_if_needed,
set_python_min,
sort_reqs,
update_recipe,
)
Expand Down Expand Up @@ -300,6 +301,7 @@ def test_compose_test_section_with_requirements_setup(dask_sdist_metadata_setup)
"pytest-xdist",
"pytest-rerunfailures",
"pre-commit",
"python",
},
}
assert test_section == expected
Expand Down Expand Up @@ -445,6 +447,7 @@ def test_compose_test_section_with_requirements_pyproject(
"pytest-rerunfailures",
"pytest-timeout",
"pytest-xdist",
"python",
},
}
assert test_section == expected
Expand All @@ -463,7 +466,28 @@ def test_compose_test_section_with_console_scripts():
expected = {
"imports": {"pytest"},
"commands": {"pip check", "py.test --help", "pytest --help"},
"requires": {"pip"},
"requires": {"pip", "python"},
}
assert test_section == expected


def test_compose_test_section_with_requirements(dask_sdist_metadata_setup):
config = Configuration(name="dask", version="2022.7.1")
metadata = get_pypi_metadata(config)
test_requirements = dask_sdist_metadata_setup["extras_require"]["test"]
test_section = compose_test_section(metadata, test_requirements)
test_section = {k: set(v) for k, v in test_section.items()}
expected = {
"imports": {"dask"},
"commands": {"pip check", "pytest --pyargs dask"},
"requires": {
"pip",
"pytest",
"pytest-xdist",
"pytest-rerunfailures",
"pre-commit",
"python",
},
}
assert test_section == expected

Expand Down Expand Up @@ -504,7 +528,7 @@ def set_of_strings(sequence):
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"}
assert set(recipe["test"]["requires"]) == {"pip", "python"}

# all extras are included in the requirements
config = Configuration(name="dask", version="2022.6.1", extras_require_all=True)
Expand All @@ -519,7 +543,7 @@ def set_of_strings(sequence):
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"}
assert set_of_strings(recipe["test"]["requires"]) == {"pip", "python"}

# all extras are included in the requirements except for the
# test requirements which are in the test section
Expand All @@ -540,7 +564,11 @@ def set_of_strings(sequence):
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"]}
assert set_of_strings(recipe["test"]["requires"]) == {
"pip",
*extras["test"],
"python",
}

# only "array" is included in the requirements
config = Configuration(
Expand All @@ -555,7 +583,7 @@ def set_of_strings(sequence):
"Extra: array",
*extras["array"],
}
assert set_of_strings(recipe["test"]["requires"]) == {"pip"}
assert set_of_strings(recipe["test"]["requires"]) == {"pip", "python"}

# only "test" is included but in the test section
config = Configuration(
Expand All @@ -570,7 +598,11 @@ def set_of_strings(sequence):
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"]}
assert set_of_strings(recipe["test"]["requires"]) == {
"pip",
*extras["test"],
"python",
}

# only "test" is included in the test section
config = Configuration(
Expand All @@ -586,7 +618,11 @@ def set_of_strings(sequence):
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"]}
assert set_of_strings(recipe["test"]["requires"]) == {
"pip",
*extras["test"],
"python",
}

# all extras have their own output except for the
# test requirements which are in the test section
Expand Down Expand Up @@ -620,7 +656,7 @@ def set_of_strings(sequence):
found[output["name"]] = set_of_strings(output["requirements"]["run"])
assert found == expected

expected = {"pip", *extras["test"]}
expected = {"pip", *extras["test"], "python"}
assert set_of_strings(recipe["test"]["requires"]) == expected
for output in recipe["outputs"]:
if output["name"] == "dask":
Expand Down Expand Up @@ -1151,7 +1187,7 @@ def test_ciso_recipe():
["<{ pin_compatible('numpy') }}", "oldest-supported-numpy", "python >=3.9"]
)
assert recipe["test"]["commands"] == ["pip check"]
assert recipe["test"]["requires"] == ["pip"]
assert recipe["test"]["requires"] == ["pip", "python"]
assert recipe["test"]["imports"] == ["ciso"]


Expand Down Expand Up @@ -1344,20 +1380,21 @@ def test_normalize_pkg_name():
def test_mypy_deps_normalization_and_entry_points():
config = Configuration(name="mypy", version="0.770")
recipe = GrayskullFactory.create_recipe("pypi", config)
assert "mypy_extensions >=0.4.3,<0.5.0" in recipe["requirements"]["run"]
assert (
"mypy_extensions >=0.4.3,<0.5.0" in recipe["requirements"]["run"]
or "mypy_extensions <0.5.0,>=0.4.3" in recipe["requirements"]["run"]
)
assert "mypy-extensions >=0.4.3,<0.5.0" not in recipe["requirements"]["run"]
assert "typed-ast >=1.4.0,<1.5.0" in recipe["requirements"]["run"]
assert "mypy-extensions <0.5.0,>=0.4.3" not in recipe["requirements"]["run"]
assert (
"typed-ast >=1.4.0,<1.5.0" in recipe["requirements"]["run"]
or "typed-ast <1.5.0,>=1.4.0" in recipe["requirements"]["run"]
)
assert "typed_ast <1.5.0,>=1.4.0" not in recipe["requirements"]["run"]
assert "typed_ast >=1.4.0,<1.5.0" not in recipe["requirements"]["run"]
assert "typing-extensions >=3.7.4" not in recipe["requirements"]["run"]
assert "typing_extensions >=3.7.4" in recipe["requirements"]["run"]

assert recipe["build"]["entry_points"] == [
"mypy=mypy.__main__:console_entry",
"stubgen=mypy.stubgen:main",
"stubtest=mypy.stubtest:main",
"dmypy=mypy.dmypy.client:console_entry",
]


@pytest.mark.skipif(
condition=sys.platform.startswith("win"), reason="Skipping test for win"
Expand Down Expand Up @@ -1423,7 +1460,7 @@ def test_sequence_inside_another_in_dependencies(freeze_py_cf_supported):
)[0]
assert sorted(recipe["requirements"]["host"]) == sorted(
[
"python >=3.6",
"python {{ python_min }}.*",
"argparse",
"pip",
"six >=1.4",
Expand All @@ -1432,7 +1469,7 @@ def test_sequence_inside_another_in_dependencies(freeze_py_cf_supported):
)
assert sorted(recipe["requirements"]["run"]) == sorted(
[
"python >=3.6",
"python >={{ python_min }}",
"argparse",
"six >=1.4",
"traceback2",
Expand Down Expand Up @@ -1551,8 +1588,8 @@ def test_add_python_min_to_strict_conda_forge(freeze_py_cf_supported):
py_cf_supported=freeze_py_cf_supported,
)[0]
assert recipe["build"]["noarch"] == "python"
assert recipe["requirements"]["host"][0] == "python >=3.6"
assert "python >=3.6" in recipe["requirements"]["run"]
assert recipe["requirements"]["host"][0] == "python {{ python_min }}.*"
assert "python >={{ python_min }}" in recipe["requirements"]["run"]


def test_get_test_imports_clean_modules():
Expand Down Expand Up @@ -1674,7 +1711,7 @@ def test_remove_selectors_pkgs_if_needed_with_recipe():
"importlib-metadata",
"numpy >=1.17",
"packaging",
"python",
"python >={{ python_min }}",
"regex !=2019.12.17",
"requests",
"sacremoses",
Expand All @@ -1692,7 +1729,7 @@ def test_noarch_python_min_constrain(freeze_py_cf_supported):
version="0.1.1",
py_cf_supported=freeze_py_cf_supported,
)
assert recipe["requirements"]["run"] == ["python >=3.6"]
assert recipe["requirements"]["run"] == ["python >={{ python_min }}"]


def test_cpp_language_extra():
Expand Down Expand Up @@ -1942,3 +1979,27 @@ def test_compute_home():
is None
)
assert compute_home({}) is None


@pytest.mark.parametrize(
"section, expected",
[
("host", "python {{ python_min }}.*"),
("run", "python >={{ python_min }}"),
("test", "python {{ python_min }}.*"),
],
)
def test_set_python_min(section, expected):
req = ["pip", "python"]
# recipe arg shouldn't be used here
assert set_python_min(req, section, None) == ["pip", expected]

req = ["pip", "python >=3.9"]
recipe = Recipe(name="test")
assert set_python_min(req, section, recipe) == ["pip", expected]
# TODO: why there's a #% in here? the real recipe looks correct.
assert recipe[0] == '#% set python_min = "3.9" %}'

# two disjoint constraints should stop us from changing anything
req = ["pip", "python >=3.9", "python >=3.11"]
assert set_python_min(req, section, None) == req
Loading