From 1e5667d845263c0476eb51ea5993fb3c5ff149ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 14:44:52 +0000 Subject: [PATCH 1/2] fix: escape triple quotes in generated Python docstrings WIT docs containing """ were copied into Python """...""" strings, producing a SyntaxError. Choose a delimiter that is not present in the docs, or escape """ when both """ and ''' appear. Co-authored-by: Cestercian --- src/summary.rs | 10 +++++++- tests/bindings.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/summary.rs b/src/summary.rs index 9d62b6da..b2bdc5c1 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -2661,12 +2661,20 @@ fn docstring(docs: Option<&str>, indent_level: usize, error: Option<&str>) -> St .map(|_| " ") .collect::>() .concat(); + // WIT docs may contain `"""` and/or `'''`. Use a delimiter that does + // not appear in the text; if both do, escape `"""` so a `"""` wrapper + // remains valid Python. + let (quote, docs) = match (docs.contains(r#"""""#), docs.contains("'''")) { + (true, true) => (r#"""""#, docs.replace(r#"""""#, r#"\""""#)), + (true, false) => ("'''", docs), + _ => (r#"""""#, docs), + }; let docs = docs .lines() .map(|line| format!("{indent}{line}\n")) .collect::>() .concat(); - format!(r#""""{newline}{docs}{indent}"""{newline}{indent}"#) + format!("{quote}{newline}{docs}{indent}{quote}{newline}{indent}") } else { String::new() } diff --git a/tests/bindings.rs b/tests/bindings.rs index fed2a1cc..5fc49754 100644 --- a/tests/bindings.rs +++ b/tests/bindings.rs @@ -190,6 +190,66 @@ fn lint_tcp_p3_bindings() -> anyhow::Result<()> { Ok(()) } +#[test] +fn docstring_triple_quotes_are_valid_python() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs::write( + dir.path().join("example.wit"), + r#"package demo:poc; + +world example { + /// """ + export hello: func(name: string) -> string; + + /// docs containing both """ and ''' + export both: func() -> string; +} +"#, + )?; + + cargo::cargo_bin_cmd!("componentize-py") + .current_dir(dir.path()) + .args(["-d", "example.wit", "-w", "example", "bindings", "."]) + .assert() + .success(); + + assert!(predicate::path::is_dir().eval(&dir.path().join("wit_world"))); + + Command::new("python3") + .current_dir(dir.path()) + .args([ + "-c", + r#" +import ast +import sys +from pathlib import Path + +docs_by_name = {} +for path in Path(".").rglob("*.py"): + tree = ast.parse(path.read_text(), filename=str(path)) + for node in ast.walk(tree): + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + doc = ast.get_docstring(node) + if doc: + docs_by_name.setdefault(node.name, []).append(doc) + +hello_docs = docs_by_name.get("hello", []) +if not any('"""' in doc for doc in hello_docs): + sys.stderr.write("hello docstring lost triple double quotes: %r\n" % hello_docs) + sys.exit(1) + +both_docs = docs_by_name.get("both", []) +if not any(("'''" in doc and '"""' in doc) for doc in both_docs): + sys.stderr.write("both docstring lost quote sequences: %r\n" % both_docs) + sys.exit(1) +"#, + ]) + .assert() + .success(); + + Ok(()) +} + fn generate_bindings(path: &Path, world: &str) -> Result { Ok(cargo::cargo_bin_cmd!("componentize-py") .current_dir(path) From 4b1b17025579aecf68018da921e4d1381546f338 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 14:59:29 +0000 Subject: [PATCH 2/2] fix: escape leftover quotes in mixed Python docstrings Replacing """ as a sequence left a trailing quote on """", which closed the generated string. When both quote styles are present, escape backslashes first and then every double quote. Co-authored-by: Cestercian --- src/summary.rs | 6 +++--- tests/bindings.rs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/summary.rs b/src/summary.rs index b2bdc5c1..d843d90e 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -2662,10 +2662,10 @@ fn docstring(docs: Option<&str>, indent_level: usize, error: Option<&str>) -> St .collect::>() .concat(); // WIT docs may contain `"""` and/or `'''`. Use a delimiter that does - // not appear in the text; if both do, escape `"""` so a `"""` wrapper - // remains valid Python. + // not appear in the text; if both do, escape `\` then every `"` so a + // `"""` wrapper remains valid Python. let (quote, docs) = match (docs.contains(r#"""""#), docs.contains("'''")) { - (true, true) => (r#"""""#, docs.replace(r#"""""#, r#"\""""#)), + (true, true) => (r#"""""#, docs.replace('\\', "\\\\").replace('"', "\\\"")), (true, false) => ("'''", docs), _ => (r#"""""#, docs), }; diff --git a/tests/bindings.rs b/tests/bindings.rs index 5fc49754..9403ddea 100644 --- a/tests/bindings.rs +++ b/tests/bindings.rs @@ -203,6 +203,12 @@ world example { /// docs containing both """ and ''' export both: func() -> string; + + /// docs containing both """" and ''' + export four: func() -> string; + + /// docs containing both \""" and ''' + export backslash: func() -> string; } "#, )?; @@ -242,6 +248,16 @@ both_docs = docs_by_name.get("both", []) if not any(("'''" in doc and '"""' in doc) for doc in both_docs): sys.stderr.write("both docstring lost quote sequences: %r\n" % both_docs) sys.exit(1) + +four_docs = docs_by_name.get("four", []) +if not any(("'''" in doc and '""""' in doc) for doc in four_docs): + sys.stderr.write("four docstring lost quote sequences: %r\n" % four_docs) + sys.exit(1) + +backslash_docs = docs_by_name.get("backslash", []) +if not any(("'''" in doc and '\\"""' in doc) for doc in backslash_docs): + sys.stderr.write("backslash docstring lost quote sequences: %r\n" % backslash_docs) + sys.exit(1) "#, ]) .assert()