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 all commits
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
46 changes: 44 additions & 2 deletions cibuildwheel/util.py
Expand Up @@ -48,14 +48,56 @@
)


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 hash
{{ # 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,
)

# we use a lambda for repl to prevent re.sub interpreting backslashes
# in repl as escape sequences
result = re.sub(
pattern=find_pattern,
repl=lambda _: str(value),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
repl=lambda _: str(value),
repl=value.replace('\\', r'\\'),

FYI, this is the suggestion in the Python docs for escaping here. Don't have a strong preference, just wanted to point it out. See the final suggestion in https://docs.python.org/3/library/re.html#re.escape.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I missed that. Will stick to the lambda for convenience

string=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
4 changes: 4 additions & 0 deletions docs/options.md
Expand Up @@ -1259,6 +1259,10 @@ Platform-specific environment variables are also available:<br/>
« subprocess_run("cibuildwheel", "--help") »
```

## Placeholders

Some options support placeholders, like `{project}`, `{package}` or `{wheel}`, that are substituted by cibuildwheel before they are used. If, for some reason, you need to write the literal name of a placeholder, e.g. literally `{project}` in a command that would ordinarily substitute `{project}`, prefix it with a hash character - `#{project}`. This is only necessary in commands where the specific string between the curly brackets would be substituted - otherwise, strings not modified.

<style>
.options-toc {
display: grid;
Expand Down
48 changes: 48 additions & 0 deletions unit_test/utils_test.py
@@ -0,0 +1,48 @@
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 {} \\;"
)

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