Repository: pypa/cibuildwheel

Issue:
fix: pass through unexpected format strings
Closes #840.



Discussion:

Resolving patch:
diff --git a/cibuildwheel/util.py b/cibuildwheel/util.py
--- a/cibuildwheel/util.py
+++ b/cibuildwheel/util.py
@@ -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
+            """,
+            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),
+            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]:


Test patch:
diff --git a/unit_test/utils_test.py b/unit_test/utils_test.py
new file mode 100644
--- /dev/null
+++ b/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]}"
+    )