From 97e3fbbd73795d80a515878bb78c2d7be553c652 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 21 Oct 2021 15:18:01 -0400 Subject: [PATCH 1/4] fix: pass through unexpected format strings Uses a regex-based solution instead of str.format --- cibuildwheel/util.py | 42 ++++++++++++++++++++++++++++++++++++++--- unit_test/utils_test.py | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 unit_test/utils_test.py diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index 3474d3723..a4e3058fe 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -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 @@ -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""" + (? 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]: diff --git a/unit_test/utils_test.py b/unit_test/utils_test.py new file mode 100644 index 000000000..cc5013493 --- /dev/null +++ b/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]}" + ) From f4b14d3da07b643698dd4c3111ab6abebd4c0012 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Sun, 14 Nov 2021 13:38:41 +0000 Subject: [PATCH 2/4] Fix backslash replacement issue --- cibuildwheel/util.py | 8 +++++++- unit_test/utils_test.py | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index a4e3058fe..0e411351a 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -76,7 +76,13 @@ def format_safe(template: str, **kwargs: Any) -> str: re.VERBOSE, ) - result = re.sub(find_pattern, str(value), result) + # 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), + string=result, + ) # transform escaped sequences into their literal equivalents result = result.replace(f"\\{{{key}}}", f"{{{key}}}") diff --git a/unit_test/utils_test.py b/unit_test/utils_test.py index cc5013493..cdd7a2da1 100644 --- a/unit_test/utils_test.py +++ b/unit_test/utils_test.py @@ -29,6 +29,14 @@ def test_prepare_command(): == "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") From c824ed99d66675c7a87a59717ae8bac76c4f9295 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Sun, 14 Nov 2021 18:43:34 +0000 Subject: [PATCH 3/4] Use hash rather than backslash for escaping --- cibuildwheel/util.py | 4 ++-- unit_test/utils_test.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py index 5cf5d7f24..2b1c28447 100644 --- a/cibuildwheel/util.py +++ b/cibuildwheel/util.py @@ -68,7 +68,7 @@ def format_safe(template: str, **kwargs: Any) -> str: for key, value in kwargs.items(): find_pattern = re.compile( fr""" - (? str: ) # transform escaped sequences into their literal equivalents - result = result.replace(f"\\{{{key}}}", f"{{{key}}}") + result = result.replace(f"#{{{key}}}", f"{{{key}}}") return result diff --git a/unit_test/utils_test.py b/unit_test/utils_test.py index cdd7a2da1..41e376fa3 100644 --- a/unit_test/utils_test.py +++ b/unit_test/utils_test.py @@ -3,8 +3,8 @@ 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}}" + 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" @@ -15,6 +15,10 @@ def test_format_safe(): == "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" From c3e38ca6c67b4a014a7723beff740caac547a11c Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Sun, 14 Nov 2021 19:01:07 +0000 Subject: [PATCH 4/4] Add note in docs about placeholders --- docs/options.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/options.md b/docs/options.md index 3b9aaeb36..53ff1c4f5 100644 --- a/docs/options.md +++ b/docs/options.md @@ -1259,6 +1259,10 @@ Platform-specific environment variables are also available:
« 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. +