Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2661,12 +2661,20 @@ fn docstring(docs: Option<&str>, indent_level: usize, error: Option<&str>) -> St
.map(|_| " ")
.collect::<Vec<_>>()
.concat();
// WIT docs may contain `"""` and/or `'''`. Use a delimiter that does
// 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('\\', "\\\\").replace('"', "\\\"")),
(true, false) => ("'''", docs),
_ => (r#"""""#, docs),
};
let docs = docs
.lines()
.map(|line| format!("{indent}{line}\n"))
.collect::<Vec<_>>()
.concat();
format!(r#""""{newline}{docs}{indent}"""{newline}{indent}"#)
format!("{quote}{newline}{docs}{indent}{quote}{newline}{indent}")
} else {
String::new()
}
Expand Down
76 changes: 76 additions & 0 deletions tests/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,82 @@ 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;

/// docs containing both """" and '''
export four: func() -> string;

/// docs containing both \""" and '''
export backslash: 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)

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()
.success();

Ok(())
}

fn generate_bindings(path: &Path, world: &str) -> Result<Assert, anyhow::Error> {
Ok(cargo::cargo_bin_cmd!("componentize-py")
.current_dir(path)
Expand Down
Loading