Files
cibuildwheel/unit_test/utils_test.py
T
Agriya KhetarpalHood ChathamJoe RickerbyJoe RickerbyHenry Schreinerpre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
163864158d feat: Pyodide improvements: version setting, standalone environments (#2002)
* Fix a typo: pyoodide ➡️ pyodide

* Add `pyodide_build_version` attribute

* Add version to xbuildenv log step

* Add version to Emscripten log step

* Use `pyodide-build`'s version for updating constraints

* Bump Pyodide constraints by updating `pyodide-build`

* Add a schema for `pyodide-version`

* Update Pyodide constraints

* Bump `pyodide-build` to new 0.29.0

* Test out another Pyodide identifier

* Update outdated Pyodide constraints

* Add Pyodide version to temp directory name

* Remove Pyodide 0.26.1 from build configurations

* Retrieve + validate + install specific xbuildenvs

* Test wheel builds with Pyodide 0.26.2

* Add correct Pyodide version to identifier temp dir

* Don't pre-call Pyodide xbuildenv search

* Fetch just the stable Pyodide versions

* Refactor search + validation + install into one step

* Move all of it under a lock

* Reorder xbuildenv installation

* Add env and cwd to xbuildenv search call

* Temporarily lower to 0.26.2 target

* Separate out search, validate, install; again

* Run xbuildenv search in `CIBW_CACHE_PATH`

* Remove prior `PYODIDE_ROOT` env vars, copy envs

* Validate doesn't need to depend on searching

* Add file lock when searching xbuildenvs

* Test the original version: 0.26.1

* Update Pyodide constraints

* Update constraints for `pyodide-build` 0.29.0 again

* Bump Pyodide from version 0.26.1 ➡️ version 0.26.4

* Add note on compatibility for macOS + other archs

* Note Pyodide version for Pyodide identifier

* Docs about `CIBW_PYODIDE_VERSION`

* Don't fetch just the stable versions

* Discard a variable that's not used later

* Rename `search_xbuildenv` ➡️ `get_xbuildenv_versions`

* `validate_xbuildenv` ➡️ `validate_xbuildenv_version`

* Replace ordered comment, add newline

* Replace sentence on macOS support

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Capitalise: "pyodide" ➡️ "Pyodide"

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* "work" ➡️ "may succeed"

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Add another job to test a custom Pyodide version

* Handle "v"-prefixed + non-prefixed versions

* Convert to a proper toml-able option, and remove some hardcoded versions

This removes the enscripten and pyodide-build version specs from
pyproject.toml - pyodide-build is spec'd in the constraints file, and
the emscripten version can be read from the pyodide-build output.

* Add a schema entry

* Add docs for CIBW_PYODIDE_VERSION

* Rephrase

* Add tests for pyodide-version

* Apply suggestions from code review

* Add python_build_standalone util

* Hook up to python-build-standalone, removing dependency on host python

* Remove python hard-code in action.yml

* Add log step

* Add workaround for https://github.com/pyodide/pyodide-build/issues/143

* Add emscripten pytest test

* Remove unneeded checks

* Fix a pytest invoke for emscripten

* Generate pyodide-build constraints from the pinned pyodide version

* Remove pyodide python-build-standalone workaround

* Fixup paths from newer version of pyodide-build

* Fix/skip some failing tests

* Use `python -m pytest` on pyodide, even on Linux

* Docs fixes

* Don't call the github API at runtime, cache the release assets instead

* Ignore pylint false positive

* Add version auto-updating for pyodide

* Add support for pyodide 3.13.

* Fix tests for multiple pyodide wheels

* Remove workaround for unreleased pyodide-build

* Rename to "test_pyodide"

* Fix pathname confusion

* Remove extra github actions job

* Fix expectation for test_abi_none

* Fix the custom_repair_wheel test to actually have clashing names

* Fix pinned version test

* Document test-command limitation

* Remove pyodide 0.28.0a1 for now

* Update constraints files

* Docs/test fixes post removing pyodide cp313

* Fix ABI test expectation

* Docs improvements

* Improve some comments

* Remove logic duplication

* remove pyodide special casing

* Refactor constraints code to use a utility script, circumventing import issues

* chore: nicer nox env

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: typo in variable name found by copilot

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* Apply suggestions from code review

* Some more Pyodide version updates in the docs section

* We haven't released Pyodide v0.27.6 yet

* Back to the Github URL for cross-build-environments

* `pyodide-build`, not `emsdk` for Windows skips

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Move to a separate `_json_request` function

Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com>

* Rename "retries" ➡️ "retry_count"

* Add some type hints

* Copy env vars before `UV_CUSTOM_COMPILE_COMMAND`

* Use `HTTPError.headers.get` instead

* Remove extra end quote

* Change download tests URL to `https://badssl.com/`

Co-Authored-By: Joe Rickerby <1244307+joerick@users.noreply.github.com>

* Download size changes, too

* Use jsdelivr for Github asset mirroring

* Bump to Pyodide v0.27.6

* Fix unit tests

* Move to pyodide v0.27.6 again

* Bump to pyodide-build 0.30.4

* Use new URL for cross-build environments metadata

Co-authored-by: Joe Rickerby <joerick@mac.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <1244307+joerick@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-05-20 16:59:58 -04:00

398 lines
13 KiB
Python

import textwrap
from pathlib import PurePath
from unittest.mock import Mock, call
import pytest
from cibuildwheel import errors
from cibuildwheel.ci import fix_ansi_codes_for_github_actions
from cibuildwheel.util.file import copy_test_sources
from cibuildwheel.util.helpers import (
FlexibleVersion,
format_safe,
parse_key_value_string,
prepare_command,
unwrap,
unwrap_preserving_paragraphs,
)
from cibuildwheel.util.packaging import find_compatible_wheel
def test_format_safe():
assert format_safe("{wheel}", wheel="filename.whl") == "filename.whl"
assert format_safe("command #{wheel}", wheel="filename.whl") == "command {wheel}"
assert format_safe("{command #{wheel}}", wheel="filename.whl") == "{command {wheel}}"
# check unmatched brackets
assert format_safe("{command {wheel}", wheel="filename.whl") == "{command filename.whl"
# check positional-style arguments i.e. {}
assert (
format_safe("find . -name * -exec ls -a {} \\;", project="/project")
== "find . -name * -exec ls -a {} \\;"
)
assert format_safe("{param} {param}", param="1") == "1 1"
assert format_safe("# {param} {param}", param="1") == "# 1 1"
assert format_safe("#{not_a_param} {param}", param="1") == "#{not_a_param} 1"
def test_prepare_command():
assert prepare_command("python -m {project}", project="project") == "python -m project"
assert prepare_command("python -m {something}", project="project") == "python -m {something}"
assert (
prepare_command("python -m {something.abc}", project="project")
== "python -m {something.abc}"
)
assert (
prepare_command("python -m {something.abc[4]:3f}", project="project")
== "python -m {something.abc[4]:3f}"
)
# test backslashes in the replacement
assert (
prepare_command(
"command {wheel} \\Users\\Temp\\output_dir", wheel="\\Temporary Files\\cibw"
)
== "command \\Temporary Files\\cibw \\Users\\Temp\\output_dir"
)
# test some unusual syntax that used to trip up the str.format approach
assert (
prepare_command("{a}{a,b}{b:.2e}{c}{d%s}{e:3}{f[0]}", a="42", b="3.14159")
== "42{a,b}{b:.2e}{c}{d%s}{e:3}{f[0]}"
)
@pytest.mark.parametrize(
("wheel", "identifier"),
[
("foo-0.1-cp38-abi3-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-cp38-abi3-macosx_11_0_x86_64.whl", "cp310-macosx_x86_64"),
("foo-0.1-cp38-abi3-manylinux2014_x86_64.whl", "cp310-manylinux_x86_64"),
("foo-0.1-cp38-abi3-musllinux_1_1_x86_64.whl", "cp310-musllinux_x86_64"),
("foo-0.1-py2.py3-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py2.py3-none-win_amd64.whl", "pp310-win_amd64"),
("foo-0.1-py3-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "pp310-win_amd64"),
],
)
def test_find_compatible_wheel_found(wheel: str, identifier: str) -> None:
wheel_ = PurePath(wheel)
found = find_compatible_wheel([wheel_], identifier)
assert found is wheel_
@pytest.mark.parametrize(
("wheel", "identifier"),
[
("foo-0.1-cp38-abi3-win_amd64.whl", "cp310-win32"),
("foo-0.1-cp38-abi3-win_amd64.whl", "cp37-win_amd64"),
("foo-0.1-cp38-abi3-macosx_11_0_x86_64.whl", "cp310-macosx_universal2"),
("foo-0.1-cp38-abi3-manylinux2014_x86_64.whl", "cp310-musllinux_x86_64"),
("foo-0.1-cp38-abi3-musllinux_1_1_x86_64.whl", "cp310-manylinux_x86_64"),
("foo-0.1-py2-none-win_amd64.whl", "cp310-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "cp37-win_amd64"),
("foo-0.1-py38-none-win_amd64.whl", "pp37-win_amd64"),
("foo-0.1-cp38-cp38-win_amd64.whl", "cp310-win_amd64"),
],
)
def test_find_compatible_wheel_not_found(wheel: str, identifier: str) -> None:
assert find_compatible_wheel([PurePath(wheel)], identifier) is None
def test_fix_ansi_codes_for_github_actions():
input = textwrap.dedent(
"""
This line is normal
\033[1mThis line is bold
This line is also bold
\033[31m this line is red and bold
This line is red and bold, too\033[0m
This line is normal again
"""
)
expected = textwrap.dedent(
"""
This line is normal
\033[1mThis line is bold
\033[1mThis line is also bold
\033[1m\033[31m this line is red and bold
\033[1m\033[31mThis line is red and bold, too\033[0m
This line is normal again
"""
)
output = fix_ansi_codes_for_github_actions(input)
assert output == expected
def test_parse_key_value_string():
assert parse_key_value_string("bar", positional_arg_names=["foo"]) == {"foo": ["bar"]}
assert parse_key_value_string("foo:bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
with pytest.raises(ValueError, match="Too many positional arguments"):
parse_key_value_string("bar")
with pytest.raises(ValueError, match="Unknown field name"):
parse_key_value_string("foo:bar")
assert parse_key_value_string("foo:bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
assert parse_key_value_string("foo:bar", positional_arg_names=["foo"]) == {"foo": ["bar"]}
assert parse_key_value_string("foo: bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
assert parse_key_value_string("foo: bar", kw_arg_names=["foo"]) == {"foo": ["bar"]}
assert parse_key_value_string("foo: bar; baz: qux", kw_arg_names=["foo", "baz"]) == {
"foo": ["bar"],
"baz": ["qux"],
}
# some common options
assert parse_key_value_string(
"docker; create_args: --some-option --another-option=foo",
positional_arg_names=["name"],
kw_arg_names=["create_args"],
) == {
"name": ["docker"],
"create_args": ["--some-option", "--another-option=foo"],
}
# semicolon in value
assert parse_key_value_string(
"docker; create_args: --some-option='this; that'",
positional_arg_names=["name"],
kw_arg_names=["create_args"],
) == {
"name": ["docker"],
"create_args": ["--some-option=this; that"],
}
# colon in value
assert parse_key_value_string(
"docker; create_args: --mount a:b",
positional_arg_names=["name"],
kw_arg_names=["create_args"],
) == {
"name": ["docker"],
"create_args": ["--mount", "a:b"],
}
assert parse_key_value_string(
"docker;create_args:--mount a:b",
positional_arg_names=["name"],
kw_arg_names=["create_args"],
) == {
"name": ["docker"],
"create_args": ["--mount", "a:b"],
}
# quoted value with spaces
assert parse_key_value_string(
"docker;create_args:'some string with spaces'",
positional_arg_names=["name"],
kw_arg_names=["create_args"],
) == {
"name": ["docker"],
"create_args": ["some string with spaces"],
}
# colon in positional value
assert parse_key_value_string(
"docker; --mount a:b",
positional_arg_names=["name", "create_args"],
) == {
"name": ["docker"],
"create_args": ["--mount", "a:b"],
}
# empty option gives empty array
assert parse_key_value_string(
"docker;create_args:",
positional_arg_names=["name"],
kw_arg_names=["create_args"],
) == {
"name": ["docker"],
"create_args": [],
}
def test_flexible_version_comparisons():
assert FlexibleVersion("2.0") == FlexibleVersion("2")
assert FlexibleVersion("2.0") < FlexibleVersion("2.1")
assert FlexibleVersion("2.1") > FlexibleVersion("2")
assert FlexibleVersion("1.9.9") < FlexibleVersion("2.0")
assert FlexibleVersion("1.10") > FlexibleVersion("1.9.9")
assert FlexibleVersion("3.0.1") > FlexibleVersion("3.0")
assert FlexibleVersion("3.0") < FlexibleVersion("3.0.1")
# Suffix should not affect comparisons
assert FlexibleVersion("1.0.1-rhel") > FlexibleVersion("1.0")
assert FlexibleVersion("1.0.1-rhel") < FlexibleVersion("1.1")
assert FlexibleVersion("1.0.1") == FlexibleVersion("v1.0.1")
@pytest.fixture
def sample_project(tmp_path):
"""Create a directory structure that contains a range of files."""
project_path = tmp_path / "project"
(project_path / "src/deep").mkdir(parents=True)
(project_path / "tests/deep").mkdir(parents=True)
(project_path / "other").mkdir(parents=True)
(project_path / "pyproject.toml").write_text("A pyproject.toml file")
(project_path / "test.cfg").write_text("A test config file")
(project_path / "src/__init__.py").write_text("source init")
(project_path / "src/module.py").write_text("source module")
(project_path / "src/deep/__init__.py").write_text("deep source init")
(project_path / "tests/test_module.py").write_text("test module")
(project_path / "tests/deep/test_module.py").write_text("deep test module")
(project_path / "tests/deep/__init__.py").write_text("deep test init")
(project_path / "other/module.py").write_text("other module")
return project_path
@pytest.mark.parametrize(
("test_sources", "expected", "not_expected"),
[
# Empty test_sources copies nothing.
pytest.param(
[],
[],
[
"pyproject.toml",
"test.cfg",
"other/module.py",
"src/__init__.py",
"src/module.py",
"src/deep/__init__.py",
"tests/test_module.py",
"tests/deep/__init__.py",
"tests/deep/test_module.py",
],
id="empty",
),
# Single standalone files
pytest.param(
["pyproject.toml", "tests/deep/test_module.py"],
["pyproject.toml", "tests/deep/test_module.py"],
[
"test.cfg",
"other/module.py",
"src/__init__.py",
"src/module.py",
"src/deep/__init__.py",
"tests/test_module.py",
"tests/deep/__init__.py",
],
id="single-file",
),
# A full Directory
pytest.param(
["tests"],
[
"tests/test_module.py",
"tests/deep/__init__.py",
"tests/deep/test_module.py",
],
[
"pyproject.toml",
"test.cfg",
"other/module.py",
"src/__init__.py",
"src/module.py",
"src/deep/__init__.py",
],
id="top-level-directory",
),
# A partial deep directory
pytest.param(
["tests/deep"],
[
"tests/deep/__init__.py",
"tests/deep/test_module.py",
],
[
"pyproject.toml",
"test.cfg",
"other/module.py",
"src/__init__.py",
"src/module.py",
"src/deep/__init__.py",
"tests/test_module.py",
],
id="partial-directory",
),
],
)
def test_copy_test_sources(tmp_path, sample_project, test_sources, expected, not_expected):
"""Test sources can be copied into the test directory."""
target = tmp_path / "somewhere/test_cwd"
copy_test_sources(test_sources, sample_project, target)
for path in expected:
assert (tmp_path / "somewhere/test_cwd" / path).is_file()
for path in not_expected:
assert not (tmp_path / "somewhere/test_cwd" / path).exists()
def test_copy_test_sources_missing_file(tmp_path, sample_project):
"""If test_sources references a folder that doesn't exist, an error is raised."""
with pytest.raises(
errors.FatalError,
match=r"Test source tests/does_not_exist.py does not exist.",
):
copy_test_sources(
["pyproject.toml", "tests/does_not_exist.py"],
sample_project,
tmp_path / "somewhere/test_cwd",
)
def test_copy_test_sources_alternate_copy_into(sample_project):
"""If an alternate copy_into method is provided, it is used."""
target = PurePath("/container/test_cwd")
copy_into = Mock()
copy_test_sources(["pyproject.toml", "tests"], sample_project, target, copy_into=copy_into)
copy_into.assert_has_calls(
[
call(sample_project / "pyproject.toml", target / "pyproject.toml"),
call(sample_project / "tests", target / "tests"),
],
any_order=True,
)
def test_unwrap():
assert (
unwrap("""
This is a
multiline
string
""")
== "This is a multiline string"
)
def test_unwrap_preserving_paragraphs():
assert (
unwrap("""
This is a
multiline
string
""")
== "This is a multiline string"
)
assert (
unwrap_preserving_paragraphs("""
paragraph one
paragraph two
""")
== "paragraph one\n\nparagraph two"
)