1use crate::writer::{Writer, verbatim_lines};
27use alloc::format;
28use alloc::string::{String, ToString};
29use alloc::vec::Vec;
30use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
31use zerodds_lang_cst::python::{
32 Annotated, Arg, Assign, Class, Decorator, Expr, Function, Import, ImportName, Keyword, Module,
33 Param, Statement, Type,
34};
35
36#[must_use]
38pub fn module(node: &Module) -> String {
39 let mut w = Writer::spaces4();
40 docstring(&mut w, &node.doc);
41 if !node.future.is_empty() {
42 w.blank_line();
43 let names: Vec<&str> = node.future.iter().map(Ident::as_str).collect();
44 w.line(&format!("from __future__ import {}", names.join(", ")));
45 }
46 if !node.imports.is_empty() {
47 w.blank_line();
48 for import in &node.imports {
49 w.line(&import_text(import));
50 }
51 }
52 render_body(&mut w, &node.body, true);
53 w.finish()
54}
55
56#[must_use]
58pub fn statement(node: &Statement) -> String {
59 let mut w = Writer::spaces4();
60 render_statement(&mut w, node);
61 w.finish()
62}
63
64#[must_use]
66pub fn type_(node: &Type) -> String {
67 type_text(node)
68}
69
70#[must_use]
72pub fn expr(node: &Expr) -> String {
73 expr_text(node)
74}
75
76fn import_text(import: &Import) -> String {
77 match import {
78 Import::Module { path, alias } => {
79 let mut out = format!("import {}", path_text(path));
80 if let Some(alias) = alias {
81 out.push_str(" as ");
82 out.push_str(alias.as_str());
83 }
84 out
85 }
86 Import::From { path, names } => {
87 let parts: Vec<String> = names.iter().map(import_name_text).collect();
88 format!("from {} import {}", path_text(path), parts.join(", "))
89 }
90 }
91}
92
93fn import_name_text(name: &ImportName) -> String {
94 match &name.alias {
95 Some(alias) => format!("{} as {}", name.name.as_str(), alias.as_str()),
96 None => name.name.as_str().to_string(),
97 }
98}
99
100fn render_body(w: &mut Writer, body: &[Statement], top: bool) {
107 let gap = |w: &mut Writer| {
108 w.blank_line();
109 if top {
110 w.newline();
111 }
112 };
113 let mut previous_was_definition = false;
114 for (index, node) in body.iter().enumerate() {
115 let definition = matches!(node, Statement::Class(_) | Statement::Function(_));
116 if (index > 0 && (definition || previous_was_definition)) || (index == 0 && top) {
117 gap(w);
118 }
119 render_statement(w, node);
120 previous_was_definition = definition;
121 }
122}
123
124fn render_suite(w: &mut Writer, body: &[Statement]) {
133 w.indent();
134 if body.is_empty() {
135 w.line("pass");
136 } else {
137 render_body(w, body, false);
138 }
139 w.dedent();
140}
141
142fn render_statement(w: &mut Writer, node: &Statement) {
149 match node {
150 Statement::Class(class) => render_class(w, class),
151 Statement::Function(function) => render_function(w, function),
152 Statement::Annotated(binding) => render_annotated(w, binding),
153 Statement::Assign(binding) => render_assign(w, binding),
154 Statement::Set { place, value } => {
155 w.line(&format!("{} = {}", expr_text(place), expr_text(value)));
156 }
157 Statement::Expr(value) => w.line(&expr_text(value)),
158 Statement::If { .. } => render_if(w, node, "if"),
163 Statement::For {
164 counter,
165 bound,
166 body,
167 } => {
168 w.line(&format!(
172 "for {} in range({}):",
173 counter.as_str(),
174 expr_text(bound)
175 ));
176 render_suite(w, body);
177 }
178 Statement::While { condition, body } => {
179 w.line(&format!("while {}:", expr_text(condition)));
180 render_suite(w, body);
181 }
182 Statement::Break => w.line("break"),
183 Statement::Return(value) => match value {
184 Some(value) => w.line(&format!("return {}", expr_text(value))),
185 None => w.line("return"),
186 },
187 Statement::Comment(doc) => comment(w, doc),
188 Statement::Pass => w.line("pass"),
189 Statement::Verbatim(text) => verbatim_lines(w, text),
190 }
191}
192
193fn render_if(w: &mut Writer, node: &Statement, keyword: &str) {
228 let mut node = node;
229 let mut keyword = keyword;
230 loop {
231 let Statement::If {
232 condition,
233 then,
234 otherwise,
235 } = node
236 else {
237 render_statement(w, node);
241 return;
242 };
243 w.line(&format!("{keyword} {}:", expr_text(condition)));
244 render_suite(w, then);
245 let Some(otherwise) = otherwise else { return };
246 if let [nested @ Statement::If { .. }] = otherwise.as_slice() {
250 node = nested;
251 keyword = "elif";
252 continue;
253 }
254 w.line("else:");
255 render_suite(w, otherwise);
256 return;
257 }
258}
259
260fn render_function(w: &mut Writer, node: &Function) {
261 for decorator in &node.decorators {
262 w.line(&decorator_text(decorator));
263 }
264 let params: Vec<String> = node.params.iter().map(param_text).collect();
265 let mut head = format!("def {}({})", node.name.as_str(), params.join(", "));
266 if let Some(returns) = &node.returns {
267 head.push_str(" -> ");
268 head.push_str(&type_text(returns));
269 }
270 head.push(':');
271 w.line(&head);
272 w.indent();
273 docstring(w, &node.doc);
274 if node.body.is_empty() {
278 w.line("pass");
279 } else {
280 render_body(w, &node.body, false);
281 }
282 w.dedent();
283}
284
285fn param_text(node: &Param) -> String {
286 let mut out = node.name.as_str().to_string();
287 if let Some(type_) = &node.type_ {
288 out.push_str(": ");
289 out.push_str(&type_text(type_));
290 }
291 if let Some(default) = &node.default {
292 out.push_str(if node.type_.is_some() { " = " } else { "=" });
297 out.push_str(&expr_text(default));
298 }
299 out
300}
301
302fn render_class(w: &mut Writer, node: &Class) {
303 for decorator in &node.decorators {
304 w.line(&decorator_text(decorator));
305 }
306 let mut head = format!("class {}", node.name.as_str());
307 if !node.bases.is_empty() {
308 let parts: Vec<String> = node.bases.iter().map(type_text).collect();
309 head.push('(');
310 head.push_str(&parts.join(", "));
311 head.push(')');
312 }
313 head.push(':');
314 w.line(&head);
315 w.indent();
316 docstring(w, &node.doc);
317 if !node.doc.is_empty() && !node.body.is_empty() {
322 w.blank_line();
323 }
324 render_body(w, &node.body, false);
327 w.dedent();
328}
329
330fn render_annotated(w: &mut Writer, node: &Annotated) {
331 comment(w, &node.doc);
332 let mut line = format!("{}: {}", node.name.as_str(), type_text(&node.type_));
333 if let Some(value) = &node.value {
334 line.push_str(" = ");
335 line.push_str(&expr_text(value));
336 }
337 w.line(&line);
338}
339
340fn render_assign(w: &mut Writer, node: &Assign) {
341 comment(w, &node.doc);
342 w.line(&format!(
343 "{} = {}",
344 node.name.as_str(),
345 expr_text(&node.value)
346 ));
347}
348
349fn decorator_text(node: &Decorator) -> String {
350 let mut out = format!("@{}", path_text(&node.path));
351 if !node.args.is_empty() {
355 out.push('(');
356 let parts: Vec<String> = node.args.iter().map(arg_text).collect();
357 out.push_str(&parts.join(", "));
358 out.push(')');
359 }
360 out
361}
362
363fn arg_text(node: &Arg) -> String {
364 match node {
365 Arg::Positional(value) => expr_text(value),
366 Arg::Keyword(keyword) => keyword_text(keyword),
367 }
368}
369
370fn keyword_text(node: &Keyword) -> String {
371 format!("{}={}", node.name.as_str(), expr_text(&node.value))
372}
373
374fn path_text(path: &Path) -> String {
383 let parts: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
384 parts.join(".")
385}
386
387fn type_text(node: &Type) -> String {
388 match node {
389 Type::Name(path) => path_text(path),
390 Type::Subscript { base, args } => {
391 let parts: Vec<String> = args.iter().map(type_text).collect();
392 format!("{}[{}]", path_text(base), parts.join(", "))
393 }
394 Type::Optional(inner) => format!("typing.Optional[{}]", type_text(inner)),
395 Type::Annotated { base, markers } => {
396 let mut parts = alloc::vec![type_text(base)];
397 parts.extend(markers.iter().map(expr_text));
398 format!("typing.Annotated[{}]", parts.join(", "))
399 }
400 }
401}
402
403fn expr_text(node: &Expr) -> String {
404 match node {
405 Expr::Literal(literal) => literal_text(literal),
406 Expr::Path(path) => path_text(path),
407 Expr::Call { func, args } => {
408 let parts: Vec<String> = args.iter().map(arg_text).collect();
409 format!("{}({})", path_text(func), parts.join(", "))
410 }
411 Expr::Tuple(items) => {
414 let parts: Vec<String> = items.iter().map(expr_text).collect();
415 if parts.len() == 1 {
416 format!("({},)", parts[0])
417 } else {
418 format!("({})", parts.join(", "))
419 }
420 }
421 Expr::List(items) => {
422 let parts: Vec<String> = items.iter().map(expr_text).collect();
423 format!("[{}]", parts.join(", "))
424 }
425 Expr::Attribute { value, name } => {
426 format!("{}.{}", expr_text(value), name.as_str())
427 }
428 Expr::Index { value, index } => {
429 format!("{}[{}]", expr_text(value), expr_text(index))
430 }
431 Expr::Slice { start, end } => {
432 let side = |bound: &Option<alloc::boxed::Box<Expr>>| {
433 bound.as_deref().map_or_else(String::new, expr_text)
434 };
435 format!("{}:{}", side(start), side(end))
436 }
437 Expr::Unary { op, value } => format!("{op}{}", expr_text(value)),
441 Expr::Paren(inner) => format!("({})", expr_text(inner)),
442 Expr::Dict(entries) => {
443 let parts: Vec<String> = entries
444 .iter()
445 .map(|(key, value)| format!("{}: {}", expr_text(key), expr_text(value)))
446 .collect();
447 format!("{{{}}}", parts.join(", "))
448 }
449 Expr::Binary { op, lhs, rhs } => {
452 format!("({} {op} {})", expr_text(lhs), expr_text(rhs))
453 }
454 Expr::Annotation(type_) => type_text(type_),
455 }
456}
457
458fn literal_text(literal: &Literal) -> String {
459 match literal {
460 Literal::Int(text) | Literal::Float(text) => text.clone(),
461 Literal::Bool(value) => {
462 if *value {
463 "True".to_string()
464 } else {
465 "False".to_string()
466 }
467 }
468 Literal::Char(character) => {
470 let mut text = String::new();
471 text.push(*character);
472 string_literal(&text)
473 }
474 Literal::Str(text) => string_literal(text),
475 Literal::Bytes(bytes) => {
476 let mut out = String::from("b\"");
477 for byte in bytes {
478 out.push_str(&format!("\\x{byte:02x}"));
479 }
480 out.push('"');
481 out
482 }
483 Literal::Null => "None".to_string(),
484 }
485}
486
487#[must_use]
492pub fn string_literal(text: &str) -> String {
493 let mut out = String::from("\"");
494 out.push_str(&escape_ascii(text));
495 out.push('"');
496 out
497}
498
499fn escape_ascii(text: &str) -> String {
501 let mut out = String::new();
502 for character in text.chars() {
503 match character {
504 '"' => out.push_str("\\\""),
505 '\\' => out.push_str("\\\\"),
506 '\n' => out.push_str("\\n"),
507 '\r' => out.push_str("\\r"),
508 '\t' => out.push_str("\\t"),
509 other => {
510 let point = u32::from(other);
511 if (0x20..0x7f).contains(&point) {
512 out.push(other);
513 } else if point <= 0xff {
514 out.push_str(&format!("\\x{point:02x}"));
515 } else if point <= 0xffff {
516 out.push_str(&format!("\\u{point:04x}"));
517 } else {
518 out.push_str(&format!("\\U{point:08x}"));
519 }
520 }
521 }
522 }
523 out
524}
525
526fn docstring(w: &mut Writer, doc: &Doc) {
534 if doc.is_empty() {
535 return;
536 }
537 let lines = doc.as_lines();
538 if lines.len() == 1 {
539 w.line(&format!("\"\"\"{}\"\"\"", escape_ascii(&lines[0])));
540 return;
541 }
542 w.line(&format!("\"\"\"{}", escape_ascii(&lines[0])));
543 for line in &lines[1..] {
544 if line.is_empty() {
545 w.newline();
546 } else {
547 w.line(&escape_ascii(line));
548 }
549 }
550 w.line("\"\"\"");
551}
552
553fn comment(w: &mut Writer, doc: &Doc) {
555 for line in doc.as_lines() {
556 if line.is_empty() {
557 w.line("#");
558 } else {
559 w.line(&format!("# {}", escape_ascii(line)));
560 }
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
567 use super::*;
568 use alloc::boxed::Box;
569 use alloc::vec;
570
571 fn field(name: &str, type_: Type) -> Statement {
572 Statement::Annotated(Annotated {
573 doc: Doc::none(),
574 name: Ident::new(name),
575 type_,
576 value: None,
577 })
578 }
579
580 #[test]
581 fn a_class_body_is_indented_by_four_and_closed_by_the_dedent_alone() {
582 let text = statement(&Statement::Class(Class {
586 doc: Doc::line("A point."),
587 decorators: Vec::new(),
588 name: Ident::new("Point"),
589 bases: Vec::new(),
590 body: vec![field("x", Type::name("int"))],
591 }));
592 assert_eq!(
593 text,
594 "class Point:\n \"\"\"A point.\"\"\"\n\n x: int\n"
595 );
596 }
597
598 #[test]
599 fn a_class_with_a_docstring_and_no_members_is_still_legal_python() {
600 let text = statement(&Statement::Class(Class {
601 doc: Doc::line("An IDL module."),
602 decorators: Vec::new(),
603 name: Ident::new("a"),
604 bases: Vec::new(),
605 body: Vec::new(),
606 }));
607 assert_eq!(text, "class a:\n \"\"\"An IDL module.\"\"\"\n");
608 }
609
610 #[test]
611 fn an_empty_body_takes_the_pass_the_tree_carries() {
612 let text = statement(&Statement::Class(Class {
613 doc: Doc::none(),
614 decorators: Vec::new(),
615 name: Ident::new("E"),
616 bases: vec![Type::Name(Path::new(&["enum", "IntEnum"]))],
617 body: vec![Statement::Pass],
618 }));
619 assert_eq!(text, "class E(enum.IntEnum):\n pass\n");
620 }
621
622 #[test]
623 fn an_import_carries_its_alias_and_its_from_form() {
624 assert_eq!(
625 import_text(&Import::Module {
626 path: Path::new(&["a", "b"]),
627 alias: Some(Ident::new("c")),
628 }),
629 "import a.b as c"
630 );
631 assert_eq!(
632 import_text(&Import::From {
633 path: Path::single("typing"),
634 names: vec![
635 ImportName {
636 name: Ident::new("Annotated"),
637 alias: None,
638 },
639 ImportName {
640 name: Ident::new("Optional"),
641 alias: Some(Ident::new("Maybe")),
642 },
643 ],
644 }),
645 "from typing import Annotated, Optional as Maybe"
646 );
647 }
648
649 #[test]
650 fn a_call_takes_positional_and_keyword_arguments_in_written_order() {
651 assert_eq!(
652 expr(&Expr::Call {
653 func: Path::single("f"),
654 args: vec![
655 Arg::Positional(Expr::int(1)),
656 Arg::Keyword(Keyword {
657 name: Ident::new("b"),
658 value: Expr::Literal(Literal::Bool(false)),
659 }),
660 ],
661 }),
662 "f(1, b=False)"
663 );
664 }
665
666 #[test]
667 fn a_decorator_with_no_arguments_is_written_bare() {
668 let node = Decorator {
669 path: Path::new(&["dataclasses", "dataclass"]),
670 args: Vec::new(),
671 };
672 assert_eq!(decorator_text(&node), "@dataclasses.dataclass");
673 let node = Decorator {
674 path: Path::new(&["dataclasses", "dataclass"]),
675 args: vec![Arg::Keyword(Keyword {
676 name: Ident::new("kw_only"),
677 value: Expr::Literal(Literal::Bool(true)),
678 })],
679 };
680 assert_eq!(
681 decorator_text(&node),
682 "@dataclasses.dataclass(kw_only=True)"
683 );
684 }
685
686 #[test]
687 fn an_annotated_type_keeps_its_metadata_in_the_annotation() {
688 let node = Type::Annotated {
689 base: Box::new(Type::name("int")),
690 markers: vec![Expr::Call {
691 func: Path::single("IdlInt"),
692 args: vec![
693 Arg::Keyword(Keyword {
694 name: Ident::new("bits"),
695 value: Expr::int(32),
696 }),
697 Arg::Keyword(Keyword {
698 name: Ident::new("signed"),
699 value: Expr::Literal(Literal::Bool(true)),
700 }),
701 ],
702 }],
703 };
704 assert_eq!(
705 type_(&node),
706 "typing.Annotated[int, IdlInt(bits=32, signed=True)]"
707 );
708 }
709
710 #[test]
711 fn an_exact_length_tuple_and_a_list_are_spelled_apart() {
712 assert_eq!(
716 type_(&Type::subscript(
717 Path::single("list"),
718 vec![Type::name("int")]
719 )),
720 "list[int]"
721 );
722 assert_eq!(
723 type_(&Type::subscript(
724 Path::single("tuple"),
725 vec![Type::name("int"), Type::name("int")]
726 )),
727 "tuple[int, int]"
728 );
729 }
730
731 #[test]
732 fn a_one_element_tuple_keeps_its_trailing_comma() {
733 assert_eq!(expr(&Expr::Tuple(vec![Expr::int(1)])), "(1,)");
736 assert_eq!(
737 expr(&Expr::Tuple(vec![Expr::int(1), Expr::int(2)])),
738 "(1, 2)"
739 );
740 }
741
742 #[test]
743 fn every_literal_is_written_in_the_form_python_reads_back() {
744 assert_eq!(literal_text(&Literal::Bool(true)), "True");
745 assert_eq!(literal_text(&Literal::Null), "None");
746 assert_eq!(literal_text(&Literal::Char('a')), "\"a\"");
747 assert_eq!(literal_text(&Literal::Int("-1".to_string())), "-1");
748 assert_eq!(
749 literal_text(&Literal::Bytes(vec![0x00, 0xff])),
750 "b\"\\x00\\xff\""
751 );
752 }
753
754 #[test]
755 fn a_string_literal_leaves_the_file_pure_ascii() {
756 assert_eq!(string_literal("Z"), "\"Z\"");
757 assert_eq!(string_literal("\u{e4}"), "\"\\xe4\"");
761 assert_eq!(string_literal("\u{2603}"), "\"\\u2603\"");
762 assert_eq!(string_literal("\u{1d11e}"), "\"\\U0001d11e\"");
765 assert_eq!(string_literal("a\"b\\c"), "\"a\\\"b\\\\c\"");
766 }
767
768 #[test]
769 fn a_docstring_escapes_what_would_close_it_early() {
770 let mut w = Writer::spaces4();
771 docstring(&mut w, &Doc::line("a \"quoted\" \\ backslash"));
772 assert_eq!(w.finish(), "\"\"\"a \\\"quoted\\\" \\\\ backslash\"\"\"\n");
773 }
774
775 #[test]
776 fn a_multi_line_docstring_closes_on_a_line_of_its_own() {
777 let mut w = Writer::spaces4();
778 docstring(&mut w, &Doc::lines(&["first", "", "third"]));
779 assert_eq!(w.finish(), "\"\"\"first\n\nthird\n\"\"\"\n");
780 }
781
782 #[test]
783 fn a_module_puts_the_future_import_before_everything_but_the_docstring() {
784 let text = module(&Module {
787 doc: Doc::line("Generated."),
788 future: vec![Ident::new("annotations")],
789 imports: vec![Import::Module {
790 path: Path::single("dataclasses"),
791 alias: None,
792 }],
793 body: vec![field("X", Type::name("int"))],
794 });
795 assert_eq!(
796 text,
797 "\"\"\"Generated.\"\"\"\n\nfrom __future__ import annotations\n\nimport dataclasses\n\n\nX: int\n"
798 );
799 }
800
801 #[test]
804 fn a_compound_statement_indents_its_suite_and_is_closed_by_the_dedent() {
805 let effect = |text: &str| Statement::Expr(Expr::name(text));
806 assert_eq!(
807 statement(&Statement::If {
808 condition: Expr::Binary {
809 op: "==",
810 lhs: Box::new(Expr::name("a")),
811 rhs: Box::new(Expr::int(1)),
812 },
813 then: vec![effect("f()")],
814 otherwise: Some(vec![effect("g()")]),
815 }),
816 "if (a == 1):\n f()\nelse:\n g()\n"
817 );
818 assert_eq!(
823 statement(&Statement::If {
824 condition: Expr::name("a"),
825 then: vec![effect("f()")],
826 otherwise: Some(vec![Statement::If {
827 condition: Expr::name("b"),
828 then: vec![effect("g()")],
829 otherwise: Some(vec![effect("h()")]),
830 }]),
831 }),
832 "if a:\n f()\nelif b:\n g()\nelse:\n h()\n"
833 );
834 assert_eq!(
837 statement(&Statement::If {
838 condition: Expr::name("a"),
839 then: vec![effect("f()")],
840 otherwise: Some(vec![
841 Statement::If {
842 condition: Expr::name("b"),
843 then: vec![effect("g()")],
844 otherwise: None,
845 },
846 effect("h()"),
847 ]),
848 }),
849 "if a:\n f()\nelse:\n if b:\n g()\n h()\n"
850 );
851 assert_eq!(
852 statement(&Statement::For {
853 counter: Ident::new("i"),
854 bound: Expr::name("n"),
855 body: vec![effect("f()")],
856 }),
857 "for i in range(n):\n f()\n"
858 );
859 assert_eq!(
860 statement(&Statement::While {
861 condition: Expr::name("more"),
862 body: vec![Statement::Break],
863 }),
864 "while more:\n break\n"
865 );
866 assert_eq!(
867 statement(&Statement::Return(Some(Expr::name("v")))),
868 "return v\n"
869 );
870 assert_eq!(statement(&Statement::Return(None)), "return\n");
871 }
872
873 #[test]
875 fn an_empty_suite_takes_a_pass_rather_than_rendering_to_nothing() {
876 assert_eq!(
877 statement(&Statement::If {
878 condition: Expr::name("a"),
879 then: Vec::new(),
880 otherwise: None,
881 }),
882 "if a:\n pass\n"
883 );
884 assert_eq!(
885 statement(&Statement::Function(Function {
886 doc: Doc::none(),
887 decorators: Vec::new(),
888 name: Ident::new("f"),
889 params: Vec::new(),
890 returns: None,
891 body: Vec::new(),
892 })),
893 "def f():\n pass\n"
894 );
895 }
896
897 #[test]
898 fn a_function_writes_its_annotations_and_its_return_type() {
899 let text = statement(&Statement::Function(Function {
900 doc: Doc::line("Write one sample."),
901 decorators: Vec::new(),
902 name: Ident::new("zd_enc_Point"),
903 params: vec![
904 Param {
905 name: Ident::new("w"),
906 type_: Some(Type::Name(Path::new(&["zerodds_wire", "Writer"]))),
907 default: None,
908 },
909 Param {
910 name: Ident::new("v"),
911 type_: Some(Type::Name(Path::new(&["a", "Point"]))),
912 default: None,
913 },
914 ],
915 returns: Some(Type::name("None")),
916 body: vec![Statement::Expr(Expr::Call {
917 func: Path::new(&["w", "write_i32"]),
918 args: vec![Arg::Positional(Expr::Attribute {
919 value: Box::new(Expr::name("v")),
920 name: Ident::new("x"),
921 })],
922 })],
923 }));
924 assert_eq!(
925 text,
926 "def zd_enc_Point(w: zerodds_wire.Writer, v: a.Point) -> None:\n \
927 \"\"\"Write one sample.\"\"\"\n w.write_i32(v.x)\n"
928 );
929 }
930
931 #[test]
934 fn a_default_is_spaced_only_where_the_parameter_is_annotated() {
935 assert_eq!(
936 param_text(&Param {
937 name: Ident::new("order"),
938 type_: Some(Type::name("str")),
939 default: Some(Expr::Literal(Literal::Str("stream".to_string()))),
940 }),
941 "order: str = \"stream\""
942 );
943 assert_eq!(
944 param_text(&Param {
945 name: Ident::new("order"),
946 type_: None,
947 default: Some(Expr::int(2)),
948 }),
949 "order=2"
950 );
951 }
952
953 #[test]
956 fn a_store_reaches_a_place_a_binding_could_not_name() {
957 assert_eq!(
958 statement(&Statement::Set {
959 place: Expr::Index {
960 value: Box::new(Expr::name("v")),
961 index: Box::new(Expr::name("i")),
962 },
963 value: Expr::int(0),
964 }),
965 "v[i] = 0\n"
966 );
967 assert_eq!(
968 statement(&Statement::Set {
969 place: Expr::Attribute {
970 value: Box::new(Expr::name("v")),
971 name: Ident::new("field"),
972 },
973 value: Expr::Literal(Literal::Null),
974 }),
975 "v.field = None\n"
976 );
977 }
978
979 #[test]
982 fn an_attribute_of_a_subscript_is_not_a_dotted_name() {
983 assert_eq!(
984 expr(&Expr::Attribute {
985 value: Box::new(Expr::Index {
986 value: Box::new(Expr::name("v")),
987 index: Box::new(Expr::int(0)),
988 }),
989 name: Ident::new("key"),
990 }),
991 "v[0].key"
992 );
993 }
994
995 #[test]
998 fn a_unary_operator_carries_the_space_python_needs_and_no_other() {
999 assert_eq!(
1000 expr(&Expr::Unary {
1001 op: "not ",
1002 value: Box::new(Expr::name("ok")),
1003 }),
1004 "not ok"
1005 );
1006 assert_eq!(
1007 expr(&Expr::Unary {
1008 op: "~",
1009 value: Box::new(Expr::name("bits")),
1010 }),
1011 "~bits"
1012 );
1013 assert_eq!(expr(&Expr::Paren(Box::new(Expr::name("a")))), "(a)");
1014 }
1015
1016 #[test]
1019 fn a_slice_writes_nothing_for_an_absent_end() {
1020 assert_eq!(
1021 expr(&Expr::Index {
1022 value: Box::new(Expr::name("v")),
1023 index: Box::new(Expr::Slice {
1024 start: None,
1025 end: Some(Box::new(Expr::int(5))),
1026 }),
1027 }),
1028 "v[:5]"
1029 );
1030 assert_eq!(
1031 expr(&Expr::Slice {
1032 start: None,
1033 end: None
1034 }),
1035 ":"
1036 );
1037 }
1038
1039 #[test]
1042 fn a_list_is_spelled_apart_from_a_tuple() {
1043 assert_eq!(
1044 expr(&Expr::List(vec![Expr::Literal(Literal::Null)])),
1045 "[None]"
1046 );
1047 assert_eq!(expr(&Expr::List(Vec::new())), "[]");
1048 assert_eq!(expr(&Expr::Tuple(vec![Expr::int(1)])), "(1,)");
1049 }
1050
1051 #[test]
1052 fn a_blank_line_never_carries_indentation() {
1053 let text = statement(&Statement::Class(Class {
1057 doc: Doc::line("Two fields."),
1058 decorators: Vec::new(),
1059 name: Ident::new("P"),
1060 bases: Vec::new(),
1061 body: vec![field("a", Type::name("int")), field("b", Type::name("int"))],
1062 }));
1063 for line in text.lines() {
1064 assert_eq!(line, line.trim_end(), "trailing whitespace: {line:?}");
1065 }
1066 }
1067}