Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative string-based approach to format string passthrough #889

Merged
merged 5 commits into from Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 39 additions & 3 deletions cibuildwheel/util.py
Expand Up @@ -12,7 +12,7 @@
from enum import Enum
from pathlib import Path
from time import sleep
from typing import Dict, Iterator, List, Optional
from typing import Any, Dict, Iterator, List, Optional

import bracex
import certifi
Expand Down Expand Up @@ -48,14 +48,50 @@
)


def format_safe(template: str, **kwargs: Any) -> str:
"""
Works similarly to `template.format(**kwargs)`, except that unmatched
fields in `template` are passed through untouched.

>>> format_safe('{a} {b}', a='123')
'123 {b}'
>>> format_safe('{a} {b[4]:3f}', a='123')
'123 {b[4]:3f}'

To avoid variable expansion, precede with a single backslash e.g.
>>> format_safe('\\{a} {b}', a='123')
'{a} {b}'
"""

result = template

for key, value in kwargs.items():
find_pattern = re.compile(
fr"""
(?<!\\) # don't match if preceded by a backslash
{{ # literal open curly bracket
{re.escape(key)} # the field name
}} # literal close curly bracket
joerick marked this conversation as resolved.
Show resolved Hide resolved
""",
re.VERBOSE,
)

result = re.sub(find_pattern, str(value), result)

# transform escaped sequences into their literal equivalents
result = result.replace(f"\\{{{key}}}", f"{{{key}}}")

return result


def prepare_command(command: str, **kwargs: PathOrStr) -> str:
"""
Preprocesses a command by expanding variables like {python}.

For example, used in the test_command option to specify the path to the
project's root.
project's root. Unmatched syntax will mostly be allowed through.
"""
return command.format(python="python", pip="pip", **kwargs)
return format_safe(command, python="python", pip="pip", **kwargs)


def get_build_verbosity_extra_flags(level: int) -> List[str]:
Expand Down
36 changes: 36 additions & 0 deletions unit_test/utils_test.py
@@ -0,0 +1,36 @@
from cibuildwheel.util import format_safe, prepare_command


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 {} \\;"
)


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 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]}"
)