Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
86 changes: 67 additions & 19 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 @@ -463,7 +464,28 @@
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):
config = Configuration(name="dask", version="2022.7.1")
metadata = get_pypi_metadata(config)
test_requirements = dask_sdist_metadata["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 +526,7 @@
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 +541,7 @@
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 +562,11 @@
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 +581,7 @@
"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 +596,11 @@
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 +616,11 @@
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 +654,7 @@
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 +1185,7 @@
["<{ 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 +1378,21 @@
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 @@ -1692,7 +1727,7 @@
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 +1977,16 @@
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"]
assert set_python_min(req, section) == ["pip", expected]
Loading