1use crate::doctree::{kinds, AttrValue, Node, Span};
47
48use super::expr::{self, parse_py_expr_stmt, PyConst, PyExpr, PyOp, PyUnaryOp};
49use super::PySigConfig;
50
51#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct PyRefContext {
58 pub module: Option<String>,
59 pub class_: Option<String>,
60 pub span: Span,
72}
73
74pub fn parse_reftarget(target: &str) -> (String, String, String, bool) {
82 let (reftype, target, title, refspecific) = parse_reftarget_impl(target, false);
83 (reftype.to_string(), target, title, refspecific)
84}
85
86fn parse_reftarget_impl(
87 reftarget: &str,
88 suppress_prefix: bool,
89) -> (&'static str, String, String, bool) {
90 let mut refspecific = false;
91 let (target, title) = if let Some(stripped) = reftarget.strip_prefix('.') {
92 refspecific = true;
93 (stripped.to_string(), stripped.to_string())
94 } else if let Some(stripped) = reftarget.strip_prefix('~') {
95 (stripped.to_string(), last_component(stripped).to_string())
96 } else if suppress_prefix {
97 (reftarget.to_string(), last_component(reftarget).to_string())
98 } else if let Some(stripped) = reftarget.strip_prefix("typing.") {
99 (reftarget.to_string(), stripped.to_string())
100 } else {
101 (reftarget.to_string(), reftarget.to_string())
102 };
103
104 let reftype = if target == "None" || target.starts_with("typing.") {
107 "obj"
108 } else {
109 "class"
110 };
111
112 (reftype, target, title, refspecific)
113}
114
115fn last_component(s: &str) -> &str {
117 s.rsplit('.').next().unwrap_or(s)
118}
119
120pub fn type_to_xref(target: &str, ctx: &PyRefContext, cfg: &PySigConfig) -> Node {
127 type_to_xref_impl(target, ctx, cfg, false)
128}
129
130fn type_to_xref_impl(
131 target: &str,
132 ctx: &PyRefContext,
133 cfg: &PySigConfig,
134 suppress_prefix: bool,
135) -> Node {
136 let (reftype, target, title, refspecific) = parse_reftarget_impl(target, suppress_prefix);
137
138 let mut node = Node::elem("pending_xref", ctx.span);
139 node.set(
142 "py:class",
143 AttrValue::Str(ctx.class_.clone().unwrap_or_else(|| "True".to_string())),
144 );
145 node.set(
146 "py:module",
147 AttrValue::Str(ctx.module.clone().unwrap_or_else(|| "True".to_string())),
148 );
149 node.set("refdomain", AttrValue::Str("py".to_string()));
150 node.set("refspecific", AttrValue::Int(i64::from(refspecific)));
153 node.set("reftarget", AttrValue::Str(target));
154 node.set("reftype", AttrValue::Str(reftype.to_string()));
155
156 if cfg.python_use_unqualified_type_names {
157 let shortname = last_component(&title).to_string();
159 for (condition, text) in [("resolved", shortname), ("*", title)] {
160 let mut cond = Node::elem("pending_xref_condition", ctx.span);
161 cond.set("condition", AttrValue::Str(condition.to_string()));
162 cond.children.push(Node::text_node(text, ctx.span));
163 node.children.push(cond);
164 }
165 } else {
166 node.children.push(Node::text_node(title, ctx.span));
167 }
168 node
169}
170
171pub fn parse_annotation(text: &str, ctx: &PyRefContext, cfg: &PySigConfig) -> Vec<Node> {
176 let fallback = || vec![type_to_xref_impl(text, ctx, cfg, false)];
177
178 let parsed = match parse_py_expr_stmt(text) {
179 Ok(Some(parsed)) => parsed,
180 Ok(None) => return Vec::new(),
184 Err(_) => return fallback(),
185 };
186 let Ok(frags) = unparse_frags(&parsed, cfg.python_display_short_literal_types) else {
187 return fallback();
188 };
189
190 let mut result: Vec<Node> = Vec::new();
195 for node in frags {
196 if node.kind == kinds::LITERAL {
197 result.extend(node.children);
200 } else if node.kind == kinds::TEXT {
201 let target = node.text.as_deref().unwrap_or("");
202 if target.trim().is_empty() {
203 result.push(node);
204 continue;
205 }
206 let suppress = result
207 .last()
208 .is_some_and(|last| last.kind == "desc_sig_punctuation" && last.astext() == "~");
209 if suppress {
210 result.pop();
211 }
212 result.push(type_to_xref_impl(target, ctx, cfg, suppress));
213 } else {
214 result.push(node);
215 }
216 }
217 result
218}
219
220struct Unsupported;
227
228fn text_frag(text: impl Into<String>) -> Node {
229 Node::text_node(text, Span::ZERO)
230}
231
232fn bitor_frags(out: &mut Vec<Node>) {
234 out.push(desc_sig_space());
235 out.push(desc_sig_punctuation("|"));
236 out.push(desc_sig_space());
237}
238
239fn const_repr(c: &PyConst) -> String {
244 let plain = match c {
245 PyConst::Str {
246 value,
247 quote,
248 u_prefix: true,
249 } => PyConst::Str {
250 value: value.clone(),
251 quote: *quote,
252 u_prefix: false,
253 },
254 other => other.clone(),
255 };
256 expr::unparse(&PyExpr::Constant(plain))
257}
258
259fn join_frags(
262 elts: &[PyExpr],
263 short_literals: bool,
264 out: &mut Vec<Node>,
265) -> Result<(), Unsupported> {
266 for (i, elt) in elts.iter().enumerate() {
267 if i > 0 {
268 out.push(desc_sig_punctuation(","));
269 out.push(desc_sig_space());
270 }
271 out.extend(unparse_frags(elt, short_literals)?);
272 }
273 Ok(())
274}
275
276fn unparse_frags(e: &PyExpr, short_literals: bool) -> Result<Vec<Node>, Unsupported> {
277 match e {
278 PyExpr::Attribute(value, attr) => {
284 let frags = unparse_frags(value, short_literals)?;
285 let first = frags.first().ok_or(Unsupported)?;
286 let base = first.text.as_deref().ok_or(Unsupported)?;
287 Ok(vec![text_frag(format!("{base}.{attr}"))])
288 }
289 PyExpr::BoolOp { .. } => Err(Unsupported),
293 PyExpr::BinOp { left, op, right } => {
296 if *op != PyOp::BitOr {
297 return Err(Unsupported);
298 }
299 let mut out = unparse_frags(left, short_literals)?;
300 bitor_frags(&mut out);
301 out.extend(unparse_frags(right, short_literals)?);
302 Ok(out)
303 }
304 PyExpr::Constant(c) => Ok(vec![match c {
305 PyConst::Ellipsis => desc_sig_punctuation("..."),
306 PyConst::True => desc_sig_keyword("True"),
307 PyConst::False => desc_sig_keyword("False"),
308 PyConst::Int(digits) => desc_sig_literal_number(digits),
309 PyConst::Str { .. } => desc_sig_literal_string(&const_repr(c)),
310 PyConst::None => text_frag("None"),
314 PyConst::Float(_) | PyConst::Bytes(_) => text_frag(const_repr(c)),
315 }]),
316 PyExpr::Starred(value) => {
318 let mut out = vec![desc_sig_operator("*")];
319 out.extend(unparse_frags(value, short_literals)?);
320 Ok(out)
321 }
322 PyExpr::List(elts) => {
323 let mut out = vec![desc_sig_punctuation("[")];
324 join_frags(elts, short_literals, &mut out)?;
325 out.push(desc_sig_punctuation("]"));
326 Ok(out)
327 }
328 PyExpr::Name(id) => Ok(vec![text_frag(id.clone())]),
329 PyExpr::Subscript { value, slice } => {
330 if let PyExpr::Name(id) = value.as_ref() {
334 if id == "Optional" || id == "Union" || (short_literals && id == "Literal") {
335 return unparse_pep_604(id, slice, short_literals);
336 }
337 }
338 let mut out = unparse_frags(value, short_literals)?;
339 out.push(desc_sig_punctuation("["));
340 out.extend(unparse_frags(slice, short_literals)?);
341 out.push(desc_sig_punctuation("]"));
342
343 let is_literal = matches!(
347 out[0].text.as_deref(),
348 Some("Literal") | Some("typing.Literal")
349 );
350 if is_literal {
351 for node in &mut out[1..] {
352 if node.kind == kinds::TEXT {
353 let mut wrapper = Node::elem(kinds::LITERAL, Span::ZERO);
354 wrapper.children.push(std::mem::replace(
355 node,
356 Node::elem(kinds::LITERAL, Span::ZERO),
357 ));
358 *node = wrapper;
359 }
360 }
361 }
362 Ok(out)
363 }
364 PyExpr::UnaryOp { op, operand } => {
367 let punct = match op {
368 PyUnaryOp::Invert => desc_sig_punctuation("~"),
369 PyUnaryOp::USub => desc_sig_punctuation("-"),
370 PyUnaryOp::UAdd | PyUnaryOp::Not => return Err(Unsupported),
371 };
372 let mut out = vec![punct];
373 out.extend(unparse_frags(operand, short_literals)?);
374 Ok(out)
375 }
376 PyExpr::Tuple(elts) => {
377 if elts.is_empty() {
378 Ok(vec![desc_sig_punctuation("("), desc_sig_punctuation(")")])
379 } else {
380 let mut out = Vec::new();
381 join_frags(elts, short_literals, &mut out)?;
382 Ok(out)
383 }
384 }
385 PyExpr::Call { func, args, kwargs } => {
389 let mut out = unparse_frags(func, short_literals)?;
390 out.push(desc_sig_punctuation("("));
391 let mut inner = Vec::new();
392 join_frags(args, short_literals, &mut inner)?;
393 for (name, value) in kwargs {
394 if !inner.is_empty() {
395 inner.push(desc_sig_punctuation(","));
396 inner.push(desc_sig_space());
397 }
398 inner.push(desc_sig_name(name));
399 inner.push(desc_sig_operator("="));
400 inner.extend(unparse_frags(value, short_literals)?);
401 }
402 out.extend(inner);
403 out.push(desc_sig_punctuation(")"));
404 Ok(out)
405 }
406 PyExpr::Set(_) | PyExpr::Dict(_) => Err(Unsupported),
408 }
409}
410
411fn unparse_pep_604(
416 value_id: &str,
417 slice: &PyExpr,
418 short_literals: bool,
419) -> Result<Vec<Node>, Unsupported> {
420 let mut out = Vec::new();
421 match slice {
422 PyExpr::Tuple(elts) => {
423 let (first, rest) = elts.split_first().ok_or(Unsupported)?;
424 out.extend(unparse_frags(first, short_literals)?);
425 for elt in rest {
426 bitor_frags(&mut out);
427 out.extend(unparse_frags(elt, short_literals)?);
428 }
429 }
430 other => out.extend(unparse_frags(other, short_literals)?),
432 }
433 if value_id == "Optional" {
434 bitor_frags(&mut out);
435 out.push(text_frag("None"));
436 }
437 Ok(out)
438}
439
440fn sig_leaf(kind: &'static str, class: &str, text: &str) -> Node {
445 let mut node = Node::elem(kind, Span::ZERO);
446 node.attrs.classes.push(class.to_string());
447 node.children.push(Node::text_node(text, Span::ZERO));
448 node
449}
450
451pub(crate) fn desc_sig_space() -> Node {
454 sig_leaf("desc_sig_space", "w", " ")
455}
456
457pub(crate) fn desc_sig_name(text: &str) -> Node {
459 sig_leaf("desc_sig_name", "n", text)
460}
461
462pub(crate) fn desc_sig_operator(text: &str) -> Node {
464 sig_leaf("desc_sig_operator", "o", text)
465}
466
467pub(crate) fn desc_sig_punctuation(text: &str) -> Node {
469 sig_leaf("desc_sig_punctuation", "p", text)
470}
471
472pub(crate) fn desc_sig_keyword(text: &str) -> Node {
474 sig_leaf("desc_sig_keyword", "k", text)
475}
476
477pub(crate) fn desc_sig_literal_number(text: &str) -> Node {
479 sig_leaf("desc_sig_literal_number", "m", text)
480}
481
482pub(crate) fn desc_sig_literal_string(text: &str) -> Node {
484 sig_leaf("desc_sig_literal_string", "s", text)
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 fn wrap(kind: &'static str, children: Vec<Node>) -> String {
496 let mut parent = Node::elem(kind, Span::ZERO);
497 parent.set("xml:space", AttrValue::Str("preserve".to_string()));
498 parent.children = children;
499 parent.pformat()
500 }
501
502 fn returns(annotation: &str) -> String {
505 returns_with(annotation, &PySigConfig::default())
506 }
507
508 fn returns_with(annotation: &str, cfg: &PySigConfig) -> String {
509 wrap(
510 "desc_returns",
511 parse_annotation(annotation, &PyRefContext::default(), cfg),
512 )
513 }
514
515 fn type_option(annotation: &str) -> String {
520 let mut children = vec![desc_sig_punctuation(":"), desc_sig_space()];
521 children.extend(parse_annotation(
522 annotation,
523 &PyRefContext::default(),
524 &PySigConfig::default(),
525 ));
526 wrap("desc_annotation", children)
527 }
528
529 fn unqualified() -> PySigConfig {
530 PySigConfig {
531 python_use_unqualified_type_names: true,
532 ..PySigConfig::default()
533 }
534 }
535
536 fn short_literals() -> PySigConfig {
537 PySigConfig {
538 python_display_short_literal_types: true,
539 ..PySigConfig::default()
540 }
541 }
542
543 #[test]
548 fn parse_reftarget_plain_name_is_class() {
549 assert_eq!(
550 parse_reftarget("pkg.Cls"),
551 (
552 "class".to_string(),
553 "pkg.Cls".to_string(),
554 "pkg.Cls".to_string(),
555 false
556 )
557 );
558 assert_eq!(
559 parse_reftarget("int"),
560 (
561 "class".to_string(),
562 "int".to_string(),
563 "int".to_string(),
564 false
565 )
566 );
567 }
568
569 #[test]
572 fn parse_reftarget_leading_dot_sets_refspecific() {
573 assert_eq!(
574 parse_reftarget(".MyClass"),
575 (
576 "class".to_string(),
577 "MyClass".to_string(),
578 "MyClass".to_string(),
579 true
580 )
581 );
582 }
583
584 #[test]
587 fn parse_reftarget_tilde_title_is_last_component() {
588 assert_eq!(
589 parse_reftarget("~pkg.Cls"),
590 (
591 "class".to_string(),
592 "pkg.Cls".to_string(),
593 "Cls".to_string(),
594 false
595 )
596 );
597 }
598
599 #[test]
603 fn parse_reftarget_none_and_typing_targets_are_obj() {
604 assert_eq!(
605 parse_reftarget("typing.Any"),
606 (
607 "obj".to_string(),
608 "typing.Any".to_string(),
609 "Any".to_string(),
610 false
611 )
612 );
613 assert_eq!(
614 parse_reftarget("None"),
615 (
616 "obj".to_string(),
617 "None".to_string(),
618 "None".to_string(),
619 false
620 )
621 );
622 }
623
624 #[test]
629 fn a_union_renders_xref_space_pipe_space_xref() {
630 assert_eq!(
631 returns("int | None"),
632 concat!(
633 "<desc_returns xml:space=\"preserve\">\n",
634 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
635 " int\n",
636 " <desc_sig_space classes=\"w\">\n",
637 " \n",
638 " <desc_sig_punctuation classes=\"p\">\n",
639 " |\n",
640 " <desc_sig_space classes=\"w\">\n",
641 " \n",
642 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
643 " None\n",
644 )
645 );
646 }
647
648 #[test]
651 fn optional_rewrites_to_pep_604_with_obj_none() {
652 assert_eq!(returns("Optional[int]"), returns("int | None"));
653 }
654
655 #[test]
658 fn union_subscript_rewrites_to_pipes() {
659 assert_eq!(
660 returns("Union[int, str]"),
661 concat!(
662 "<desc_returns xml:space=\"preserve\">\n",
663 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
664 " int\n",
665 " <desc_sig_space classes=\"w\">\n",
666 " \n",
667 " <desc_sig_punctuation classes=\"p\">\n",
668 " |\n",
669 " <desc_sig_space classes=\"w\">\n",
670 " \n",
671 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
672 " str\n",
673 )
674 );
675 }
676
677 #[test]
682 fn optional_of_union_flattens_and_appends_none() {
683 assert_eq!(
684 returns("Optional[Union[int, str]]"),
685 concat!(
686 "<desc_returns xml:space=\"preserve\">\n",
687 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
688 " int\n",
689 " <desc_sig_space classes=\"w\">\n",
690 " \n",
691 " <desc_sig_punctuation classes=\"p\">\n",
692 " |\n",
693 " <desc_sig_space classes=\"w\">\n",
694 " \n",
695 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
696 " str\n",
697 " <desc_sig_space classes=\"w\">\n",
698 " \n",
699 " <desc_sig_punctuation classes=\"p\">\n",
700 " |\n",
701 " <desc_sig_space classes=\"w\">\n",
702 " \n",
703 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
704 " None\n",
705 )
706 );
707 }
708
709 #[test]
714 fn a_subscript_renders_value_bracket_slice_bracket() {
715 assert_eq!(
716 returns("list[str]"),
717 concat!(
718 "<desc_returns xml:space=\"preserve\">\n",
719 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"list\" reftype=\"class\">\n",
720 " list\n",
721 " <desc_sig_punctuation classes=\"p\">\n",
722 " [\n",
723 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
724 " str\n",
725 " <desc_sig_punctuation classes=\"p\">\n",
726 " ]\n",
727 )
728 );
729 }
730
731 #[test]
734 fn a_tuple_slice_joins_with_comma_and_space() {
735 assert_eq!(
736 returns("dict[str, int]"),
737 concat!(
738 "<desc_returns xml:space=\"preserve\">\n",
739 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"dict\" reftype=\"class\">\n",
740 " dict\n",
741 " <desc_sig_punctuation classes=\"p\">\n",
742 " [\n",
743 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
744 " str\n",
745 " <desc_sig_punctuation classes=\"p\">\n",
746 " ,\n",
747 " <desc_sig_space classes=\"w\">\n",
748 " \n",
749 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
750 " int\n",
751 " <desc_sig_punctuation classes=\"p\">\n",
752 " ]\n",
753 )
754 );
755 }
756
757 #[test]
761 fn nested_subscripts_recurse_flat() {
762 assert_eq!(
763 returns("dict[str, list[int]]"),
764 concat!(
765 "<desc_returns xml:space=\"preserve\">\n",
766 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"dict\" reftype=\"class\">\n",
767 " dict\n",
768 " <desc_sig_punctuation classes=\"p\">\n",
769 " [\n",
770 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
771 " str\n",
772 " <desc_sig_punctuation classes=\"p\">\n",
773 " ,\n",
774 " <desc_sig_space classes=\"w\">\n",
775 " \n",
776 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"list\" reftype=\"class\">\n",
777 " list\n",
778 " <desc_sig_punctuation classes=\"p\">\n",
779 " [\n",
780 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
781 " int\n",
782 " <desc_sig_punctuation classes=\"p\">\n",
783 " ]\n",
784 " <desc_sig_punctuation classes=\"p\">\n",
785 " ]\n",
786 )
787 );
788 }
789
790 #[test]
794 fn a_list_display_renders_punctuation_brackets() {
795 assert_eq!(
796 returns("Callable[[int, str], bool]"),
797 concat!(
798 "<desc_returns xml:space=\"preserve\">\n",
799 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Callable\" reftype=\"class\">\n",
800 " Callable\n",
801 " <desc_sig_punctuation classes=\"p\">\n",
802 " [\n",
803 " <desc_sig_punctuation classes=\"p\">\n",
804 " [\n",
805 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
806 " int\n",
807 " <desc_sig_punctuation classes=\"p\">\n",
808 " ,\n",
809 " <desc_sig_space classes=\"w\">\n",
810 " \n",
811 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
812 " str\n",
813 " <desc_sig_punctuation classes=\"p\">\n",
814 " ]\n",
815 " <desc_sig_punctuation classes=\"p\">\n",
816 " ,\n",
817 " <desc_sig_space classes=\"w\">\n",
818 " \n",
819 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"bool\" reftype=\"class\">\n",
820 " bool\n",
821 " <desc_sig_punctuation classes=\"p\">\n",
822 " ]\n",
823 )
824 );
825 }
826
827 #[test]
830 fn an_empty_tuple_slice_renders_paren_pair() {
831 assert_eq!(
832 returns("Tuple[()]"),
833 concat!(
834 "<desc_returns xml:space=\"preserve\">\n",
835 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Tuple\" reftype=\"class\">\n",
836 " Tuple\n",
837 " <desc_sig_punctuation classes=\"p\">\n",
838 " [\n",
839 " <desc_sig_punctuation classes=\"p\">\n",
840 " (\n",
841 " <desc_sig_punctuation classes=\"p\">\n",
842 " )\n",
843 " <desc_sig_punctuation classes=\"p\">\n",
844 " ]\n",
845 )
846 );
847 }
848
849 #[test]
855 fn literal_members_stay_literal_strings_next_to_a_literal_xref() {
856 assert_eq!(
857 returns("Literal['a', 'b']"),
858 concat!(
859 "<desc_returns xml:space=\"preserve\">\n",
860 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Literal\" reftype=\"class\">\n",
861 " Literal\n",
862 " <desc_sig_punctuation classes=\"p\">\n",
863 " [\n",
864 " <desc_sig_literal_string classes=\"s\">\n",
865 " 'a'\n",
866 " <desc_sig_punctuation classes=\"p\">\n",
867 " ,\n",
868 " <desc_sig_space classes=\"w\">\n",
869 " \n",
870 " <desc_sig_literal_string classes=\"s\">\n",
871 " 'b'\n",
872 " <desc_sig_punctuation classes=\"p\">\n",
873 " ]\n",
874 )
875 );
876 }
877
878 #[test]
883 fn a_none_member_of_literal_stays_bare_text() {
884 assert_eq!(
885 returns("Literal[None]"),
886 concat!(
887 "<desc_returns xml:space=\"preserve\">\n",
888 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Literal\" reftype=\"class\">\n",
889 " Literal\n",
890 " <desc_sig_punctuation classes=\"p\">\n",
891 " [\n",
892 " None\n",
893 " <desc_sig_punctuation classes=\"p\">\n",
894 " ]\n",
895 )
896 );
897 }
898
899 #[test]
904 fn typing_literal_is_obj_with_stripped_title() {
905 assert_eq!(
906 returns("typing.Literal['a']"),
907 concat!(
908 "<desc_returns xml:space=\"preserve\">\n",
909 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Literal\" reftype=\"obj\">\n",
910 " Literal\n",
911 " <desc_sig_punctuation classes=\"p\">\n",
912 " [\n",
913 " <desc_sig_literal_string classes=\"s\">\n",
914 " 'a'\n",
915 " <desc_sig_punctuation classes=\"p\">\n",
916 " ]\n",
917 )
918 );
919 }
920
921 #[test]
928 fn a_tilde_before_a_name_suppresses_the_title_prefix() {
929 assert_eq!(
930 returns("~pkg.Cls"),
931 concat!(
932 "<desc_returns xml:space=\"preserve\">\n",
933 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"pkg.Cls\" reftype=\"class\">\n",
934 " Cls\n",
935 )
936 );
937 assert_eq!(
938 returns("~Cls"),
939 concat!(
940 "<desc_returns xml:space=\"preserve\">\n",
941 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Cls\" reftype=\"class\">\n",
942 " Cls\n",
943 )
944 );
945 }
946
947 #[test]
952 fn typing_prefix_yields_obj_reftype() {
953 assert_eq!(
954 returns("typing.Any"),
955 concat!(
956 "<desc_returns xml:space=\"preserve\">\n",
957 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Any\" reftype=\"obj\">\n",
958 " Any\n",
959 )
960 );
961 }
962
963 #[test]
966 fn bare_none_annotation_is_an_obj_xref() {
967 assert_eq!(
968 returns("None"),
969 concat!(
970 "<desc_returns xml:space=\"preserve\">\n",
971 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
972 " None\n",
973 )
974 );
975 }
976
977 #[test]
982 fn ellipsis_renders_punctuation() {
983 assert_eq!(
984 returns("..."),
985 concat!(
986 "<desc_returns xml:space=\"preserve\">\n",
987 " <desc_sig_punctuation classes=\"p\">\n",
988 " ...\n",
989 )
990 );
991 }
992
993 #[test]
996 fn true_renders_keyword() {
997 assert_eq!(
998 returns("True"),
999 concat!(
1000 "<desc_returns xml:space=\"preserve\">\n",
1001 " <desc_sig_keyword classes=\"k\">\n",
1002 " True\n",
1003 )
1004 );
1005 }
1006
1007 #[test]
1010 fn an_int_renders_literal_number() {
1011 assert_eq!(
1012 returns("42"),
1013 concat!(
1014 "<desc_returns xml:space=\"preserve\">\n",
1015 " <desc_sig_literal_number classes=\"m\">\n",
1016 " 42\n",
1017 )
1018 );
1019 }
1020
1021 #[test]
1024 fn a_negative_int_renders_minus_punctuation_then_number() {
1025 assert_eq!(
1026 returns("-1"),
1027 concat!(
1028 "<desc_returns xml:space=\"preserve\">\n",
1029 " <desc_sig_punctuation classes=\"p\">\n",
1030 " -\n",
1031 " <desc_sig_literal_number classes=\"m\">\n",
1032 " 1\n",
1033 )
1034 );
1035 }
1036
1037 #[test]
1041 fn a_string_annotation_stays_literal_string_never_an_xref() {
1042 assert_eq!(
1043 returns("'MyClass'"),
1044 concat!(
1045 "<desc_returns xml:space=\"preserve\">\n",
1046 " <desc_sig_literal_string classes=\"s\">\n",
1047 " 'MyClass'\n",
1048 )
1049 );
1050 }
1051
1052 #[test]
1056 fn a_u_prefixed_string_drops_the_prefix_like_repr() {
1057 assert_eq!(
1058 returns("u'x'"),
1059 concat!(
1060 "<desc_returns xml:space=\"preserve\">\n",
1061 " <desc_sig_literal_string classes=\"s\">\n",
1062 " 'x'\n",
1063 )
1064 );
1065 }
1066
1067 #[test]
1072 fn float_and_bytes_constants_become_xrefs_via_repr_text() {
1073 assert_eq!(
1074 returns("1.5"),
1075 concat!(
1076 "<desc_returns xml:space=\"preserve\">\n",
1077 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"1.5\" reftype=\"class\">\n",
1078 " 1.5\n",
1079 )
1080 );
1081 assert_eq!(
1082 returns("b'x'"),
1083 concat!(
1084 "<desc_returns xml:space=\"preserve\">\n",
1085 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"b'x'\" reftype=\"class\">\n",
1086 " b'x'\n",
1087 )
1088 );
1089 }
1090
1091 #[test]
1097 fn a_call_renders_args_and_keywords() {
1098 assert_eq!(
1099 returns("Annotated[str, Validator(str, len=10)]"),
1100 concat!(
1101 "<desc_returns xml:space=\"preserve\">\n",
1102 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Annotated\" reftype=\"class\">\n",
1103 " Annotated\n",
1104 " <desc_sig_punctuation classes=\"p\">\n",
1105 " [\n",
1106 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
1107 " str\n",
1108 " <desc_sig_punctuation classes=\"p\">\n",
1109 " ,\n",
1110 " <desc_sig_space classes=\"w\">\n",
1111 " \n",
1112 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Validator\" reftype=\"class\">\n",
1113 " Validator\n",
1114 " <desc_sig_punctuation classes=\"p\">\n",
1115 " (\n",
1116 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
1117 " str\n",
1118 " <desc_sig_punctuation classes=\"p\">\n",
1119 " ,\n",
1120 " <desc_sig_space classes=\"w\">\n",
1121 " \n",
1122 " <desc_sig_name classes=\"n\">\n",
1123 " len\n",
1124 " <desc_sig_operator classes=\"o\">\n",
1125 " =\n",
1126 " <desc_sig_literal_number classes=\"m\">\n",
1127 " 10\n",
1128 " <desc_sig_punctuation classes=\"p\">\n",
1129 " )\n",
1130 " <desc_sig_punctuation classes=\"p\">\n",
1131 " ]\n",
1132 )
1133 );
1134 }
1135
1136 #[test]
1142 fn an_attribute_of_a_subscript_keeps_only_the_first_fragment() {
1143 assert_eq!(
1144 type_option("list[int].x"),
1145 concat!(
1146 "<desc_annotation xml:space=\"preserve\">\n",
1147 " <desc_sig_punctuation classes=\"p\">\n",
1148 " :\n",
1149 " <desc_sig_space classes=\"w\">\n",
1150 " \n",
1151 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"list.x\" reftype=\"class\">\n",
1152 " list.x\n",
1153 )
1154 );
1155 }
1156
1157 #[test]
1162 fn a_syntax_error_falls_back_to_one_xref_of_the_whole_text() {
1163 assert_eq!(
1164 type_option("List[int"),
1165 concat!(
1166 "<desc_annotation xml:space=\"preserve\">\n",
1167 " <desc_sig_punctuation classes=\"p\">\n",
1168 " :\n",
1169 " <desc_sig_space classes=\"w\">\n",
1170 " \n",
1171 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"List[int\" reftype=\"class\">\n",
1172 " List[int\n",
1173 )
1174 );
1175 }
1176
1177 #[test]
1181 fn a_leading_dot_falls_back_and_sets_refspecific() {
1182 assert_eq!(
1183 type_option(".MyClass"),
1184 concat!(
1185 "<desc_annotation xml:space=\"preserve\">\n",
1186 " <desc_sig_punctuation classes=\"p\">\n",
1187 " :\n",
1188 " <desc_sig_space classes=\"w\">\n",
1189 " \n",
1190 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"1\" reftarget=\"MyClass\" reftype=\"class\">\n",
1191 " MyClass\n",
1192 )
1193 );
1194 }
1195
1196 #[test]
1201 fn unsupported_node_shapes_fall_back_to_one_xref() {
1202 assert_eq!(
1203 returns("X + Y"),
1204 concat!(
1205 "<desc_returns xml:space=\"preserve\">\n",
1206 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"X + Y\" reftype=\"class\">\n",
1207 " X + Y\n",
1208 )
1209 );
1210 assert_eq!(
1211 returns("{1, 2}"),
1212 concat!(
1213 "<desc_returns xml:space=\"preserve\">\n",
1214 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"{1, 2}\" reftype=\"class\">\n",
1215 " {1, 2}\n",
1216 )
1217 );
1218 }
1219
1220 #[test]
1226 fn ref_context_lands_in_py_module_and_py_class_attrs() {
1227 let ctx = PyRefContext {
1228 module: Some("mymod".to_string()),
1229 class_: Some("C".to_string()),
1230 span: Span::ZERO,
1231 };
1232 assert_eq!(
1233 type_to_xref("int", &ctx, &PySigConfig::default()).pformat(),
1234 concat!(
1235 "<pending_xref py:class=\"C\" py:module=\"mymod\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
1236 " int\n",
1237 )
1238 );
1239 }
1240
1241 #[test]
1248 fn unqualified_config_emits_condition_pair() {
1249 assert_eq!(
1250 returns_with("pkg.Cls", &unqualified()),
1251 concat!(
1252 "<desc_returns xml:space=\"preserve\">\n",
1253 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"pkg.Cls\" reftype=\"class\">\n",
1254 " <pending_xref_condition condition=\"resolved\">\n",
1255 " Cls\n",
1256 " <pending_xref_condition condition=\"*\">\n",
1257 " pkg.Cls\n",
1258 )
1259 );
1260 }
1261
1262 #[test]
1266 fn unqualified_tilde_conditions_share_the_short_title() {
1267 assert_eq!(
1268 returns_with("~pkg.mod.Cls", &unqualified()),
1269 concat!(
1270 "<desc_returns xml:space=\"preserve\">\n",
1271 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"pkg.mod.Cls\" reftype=\"class\">\n",
1272 " <pending_xref_condition condition=\"resolved\">\n",
1273 " Cls\n",
1274 " <pending_xref_condition condition=\"*\">\n",
1275 " Cls\n",
1276 )
1277 );
1278 }
1279
1280 #[test]
1284 fn unqualified_typing_conditions_share_the_stripped_title() {
1285 assert_eq!(
1286 returns_with("typing.Any", &unqualified()),
1287 concat!(
1288 "<desc_returns xml:space=\"preserve\">\n",
1289 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Any\" reftype=\"obj\">\n",
1290 " <pending_xref_condition condition=\"resolved\">\n",
1291 " Any\n",
1292 " <pending_xref_condition condition=\"*\">\n",
1293 " Any\n",
1294 )
1295 );
1296 }
1297
1298 #[test]
1304 fn short_literal_types_render_pipe_chain_without_literal_xref() {
1305 assert_eq!(
1306 returns_with("Literal['a', 'b']", &short_literals()),
1307 concat!(
1308 "<desc_returns xml:space=\"preserve\">\n",
1309 " <desc_sig_literal_string classes=\"s\">\n",
1310 " 'a'\n",
1311 " <desc_sig_space classes=\"w\">\n",
1312 " \n",
1313 " <desc_sig_punctuation classes=\"p\">\n",
1314 " |\n",
1315 " <desc_sig_space classes=\"w\">\n",
1316 " \n",
1317 " <desc_sig_literal_string classes=\"s\">\n",
1318 " 'b'\n",
1319 )
1320 );
1321 }
1322
1323 #[test]
1327 fn a_short_literal_none_member_becomes_an_obj_xref() {
1328 assert_eq!(
1329 returns_with("Literal[1, 'a', None]", &short_literals()),
1330 concat!(
1331 "<desc_returns xml:space=\"preserve\">\n",
1332 " <desc_sig_literal_number classes=\"m\">\n",
1333 " 1\n",
1334 " <desc_sig_space classes=\"w\">\n",
1335 " \n",
1336 " <desc_sig_punctuation classes=\"p\">\n",
1337 " |\n",
1338 " <desc_sig_space classes=\"w\">\n",
1339 " \n",
1340 " <desc_sig_literal_string classes=\"s\">\n",
1341 " 'a'\n",
1342 " <desc_sig_space classes=\"w\">\n",
1343 " \n",
1344 " <desc_sig_punctuation classes=\"p\">\n",
1345 " |\n",
1346 " <desc_sig_space classes=\"w\">\n",
1347 " \n",
1348 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
1349 " None\n",
1350 )
1351 );
1352 }
1353
1354 #[test]
1359 fn short_literal_config_ignores_typing_literal() {
1360 assert_eq!(
1361 returns_with("typing.Literal['a', 'b']", &short_literals()),
1362 concat!(
1363 "<desc_returns xml:space=\"preserve\">\n",
1364 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Literal\" reftype=\"obj\">\n",
1365 " Literal\n",
1366 " <desc_sig_punctuation classes=\"p\">\n",
1367 " [\n",
1368 " <desc_sig_literal_string classes=\"s\">\n",
1369 " 'a'\n",
1370 " <desc_sig_punctuation classes=\"p\">\n",
1371 " ,\n",
1372 " <desc_sig_space classes=\"w\">\n",
1373 " \n",
1374 " <desc_sig_literal_string classes=\"s\">\n",
1375 " 'b'\n",
1376 " <desc_sig_punctuation classes=\"p\">\n",
1377 " ]\n",
1378 )
1379 );
1380 }
1381 #[test]
1391 fn pep_646_star_annotation_splits_the_operator() {
1392 assert_eq!(
1393 returns("*Ts"),
1394 concat!(
1395 "<desc_returns xml:space=\"preserve\">\n",
1396 " <desc_sig_operator classes=\"o\">\n",
1397 " *\n",
1398 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Ts\" reftype=\"class\">\n",
1399 " Ts\n",
1400 )
1401 );
1402 }
1403
1404 #[test]
1409 fn pep_646_star_annotation_over_a_subscript() {
1410 assert_eq!(
1411 returns("*tuple[int, ...]"),
1412 concat!(
1413 "<desc_returns xml:space=\"preserve\">\n",
1414 " <desc_sig_operator classes=\"o\">\n",
1415 " *\n",
1416 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"tuple\" reftype=\"class\">\n",
1417 " tuple\n",
1418 " <desc_sig_punctuation classes=\"p\">\n",
1419 " [\n",
1420 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
1421 " int\n",
1422 " <desc_sig_punctuation classes=\"p\">\n",
1423 " ,\n",
1424 " <desc_sig_space classes=\"w\">\n",
1425 " \n",
1426 " <desc_sig_punctuation classes=\"p\">\n",
1427 " ...\n",
1428 " <desc_sig_punctuation classes=\"p\">\n",
1429 " ]\n",
1430 )
1431 );
1432 }
1433
1434 #[test]
1438 fn exec_mode_renders_a_bare_starred_tuple() {
1439 assert_eq!(
1440 returns("*a, b"),
1441 concat!(
1442 "<desc_returns xml:space=\"preserve\">\n",
1443 " <desc_sig_operator classes=\"o\">\n",
1444 " *\n",
1445 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"a\" reftype=\"class\">\n",
1446 " a\n",
1447 " <desc_sig_punctuation classes=\"p\">\n",
1448 " ,\n",
1449 " <desc_sig_space classes=\"w\">\n",
1450 " \n",
1451 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"b\" reftype=\"class\">\n",
1452 " b\n",
1453 )
1454 );
1455 }
1456
1457 #[test]
1464 fn leading_indent_keeps_the_unstripped_text() {
1465 assert_eq!(
1466 returns(" int"),
1467 concat!(
1468 "<desc_returns xml:space=\"preserve\">\n",
1469 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\" int\" reftype=\"class\">\n",
1470 " int\n",
1471 )
1472 );
1473 }
1474
1475 #[test]
1482 fn an_empty_annotation_renders_no_nodes() {
1483 for text in ["", " ", " ", "\n", "\t"] {
1484 assert!(
1485 parse_annotation(text, &PyRefContext::default(), &PySigConfig::default())
1486 .is_empty(),
1487 "{text:?} must render no nodes"
1488 );
1489 }
1490 }
1491
1492 #[test]
1499 fn a_boolop_annotation_falls_back_to_one_xref() {
1500 assert_eq!(
1501 returns("a or b"),
1502 concat!(
1503 "<desc_returns xml:space=\"preserve\">\n",
1504 " <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"a or b\" reftype=\"class\">\n",
1505 " a or b\n",
1506 )
1507 );
1508 }
1509}