use crate::doctree::{kinds, AttrValue, Node, Span};
use super::expr::{self, parse_py_expr_stmt, PyConst, PyExpr, PyOp, PyUnaryOp};
use super::PySigConfig;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PyRefContext {
pub module: Option<String>,
pub class_: Option<String>,
pub span: Span,
}
pub fn parse_reftarget(target: &str) -> (String, String, String, bool) {
let (reftype, target, title, refspecific) = parse_reftarget_impl(target, false);
(reftype.to_string(), target, title, refspecific)
}
fn parse_reftarget_impl(
reftarget: &str,
suppress_prefix: bool,
) -> (&'static str, String, String, bool) {
let mut refspecific = false;
let (target, title) = if let Some(stripped) = reftarget.strip_prefix('.') {
refspecific = true;
(stripped.to_string(), stripped.to_string())
} else if let Some(stripped) = reftarget.strip_prefix('~') {
(stripped.to_string(), last_component(stripped).to_string())
} else if suppress_prefix {
(reftarget.to_string(), last_component(reftarget).to_string())
} else if let Some(stripped) = reftarget.strip_prefix("typing.") {
(reftarget.to_string(), stripped.to_string())
} else {
(reftarget.to_string(), reftarget.to_string())
};
let reftype = if target == "None" || target.starts_with("typing.") {
"obj"
} else {
"class"
};
(reftype, target, title, refspecific)
}
fn last_component(s: &str) -> &str {
s.rsplit('.').next().unwrap_or(s)
}
pub fn type_to_xref(target: &str, ctx: &PyRefContext, cfg: &PySigConfig) -> Node {
type_to_xref_impl(target, ctx, cfg, false)
}
fn type_to_xref_impl(
target: &str,
ctx: &PyRefContext,
cfg: &PySigConfig,
suppress_prefix: bool,
) -> Node {
let (reftype, target, title, refspecific) = parse_reftarget_impl(target, suppress_prefix);
let mut node = Node::elem("pending_xref", ctx.span);
node.set(
"py:class",
AttrValue::Str(ctx.class_.clone().unwrap_or_else(|| "True".to_string())),
);
node.set(
"py:module",
AttrValue::Str(ctx.module.clone().unwrap_or_else(|| "True".to_string())),
);
node.set("refdomain", AttrValue::Str("py".to_string()));
node.set("refspecific", AttrValue::Int(i64::from(refspecific)));
node.set("reftarget", AttrValue::Str(target));
node.set("reftype", AttrValue::Str(reftype.to_string()));
if cfg.python_use_unqualified_type_names {
let shortname = last_component(&title).to_string();
for (condition, text) in [("resolved", shortname), ("*", title)] {
let mut cond = Node::elem("pending_xref_condition", ctx.span);
cond.set("condition", AttrValue::Str(condition.to_string()));
cond.children.push(Node::text_node(text, ctx.span));
node.children.push(cond);
}
} else {
node.children.push(Node::text_node(title, ctx.span));
}
node
}
pub fn parse_annotation(text: &str, ctx: &PyRefContext, cfg: &PySigConfig) -> Vec<Node> {
let fallback = || vec![type_to_xref_impl(text, ctx, cfg, false)];
let parsed = match parse_py_expr_stmt(text) {
Ok(Some(parsed)) => parsed,
Ok(None) => return Vec::new(),
Err(_) => return fallback(),
};
let Ok(frags) = unparse_frags(&parsed, cfg.python_display_short_literal_types) else {
return fallback();
};
let mut result: Vec<Node> = Vec::new();
for node in frags {
if node.kind == kinds::LITERAL {
result.extend(node.children);
} else if node.kind == kinds::TEXT {
let target = node.text.as_deref().unwrap_or("");
if target.trim().is_empty() {
result.push(node);
continue;
}
let suppress = result
.last()
.is_some_and(|last| last.kind == "desc_sig_punctuation" && last.astext() == "~");
if suppress {
result.pop();
}
result.push(type_to_xref_impl(target, ctx, cfg, suppress));
} else {
result.push(node);
}
}
result
}
struct Unsupported;
fn text_frag(text: impl Into<String>) -> Node {
Node::text_node(text, Span::ZERO)
}
fn bitor_frags(out: &mut Vec<Node>) {
out.push(desc_sig_space());
out.push(desc_sig_punctuation("|"));
out.push(desc_sig_space());
}
fn const_repr(c: &PyConst) -> String {
let plain = match c {
PyConst::Str {
value,
quote,
u_prefix: true,
} => PyConst::Str {
value: value.clone(),
quote: *quote,
u_prefix: false,
},
other => other.clone(),
};
expr::unparse(&PyExpr::Constant(plain))
}
fn join_frags(
elts: &[PyExpr],
short_literals: bool,
out: &mut Vec<Node>,
) -> Result<(), Unsupported> {
for (i, elt) in elts.iter().enumerate() {
if i > 0 {
out.push(desc_sig_punctuation(","));
out.push(desc_sig_space());
}
out.extend(unparse_frags(elt, short_literals)?);
}
Ok(())
}
fn unparse_frags(e: &PyExpr, short_literals: bool) -> Result<Vec<Node>, Unsupported> {
match e {
PyExpr::Attribute(value, attr) => {
let frags = unparse_frags(value, short_literals)?;
let first = frags.first().ok_or(Unsupported)?;
let base = first.text.as_deref().ok_or(Unsupported)?;
Ok(vec![text_frag(format!("{base}.{attr}"))])
}
PyExpr::BoolOp { .. } => Err(Unsupported),
PyExpr::BinOp { left, op, right } => {
if *op != PyOp::BitOr {
return Err(Unsupported);
}
let mut out = unparse_frags(left, short_literals)?;
bitor_frags(&mut out);
out.extend(unparse_frags(right, short_literals)?);
Ok(out)
}
PyExpr::Constant(c) => Ok(vec![match c {
PyConst::Ellipsis => desc_sig_punctuation("..."),
PyConst::True => desc_sig_keyword("True"),
PyConst::False => desc_sig_keyword("False"),
PyConst::Int(digits) => desc_sig_literal_number(digits),
PyConst::Str { .. } => desc_sig_literal_string(&const_repr(c)),
PyConst::None => text_frag("None"),
PyConst::Float(_) | PyConst::Bytes(_) => text_frag(const_repr(c)),
}]),
PyExpr::Starred(value) => {
let mut out = vec![desc_sig_operator("*")];
out.extend(unparse_frags(value, short_literals)?);
Ok(out)
}
PyExpr::List(elts) => {
let mut out = vec![desc_sig_punctuation("[")];
join_frags(elts, short_literals, &mut out)?;
out.push(desc_sig_punctuation("]"));
Ok(out)
}
PyExpr::Name(id) => Ok(vec![text_frag(id.clone())]),
PyExpr::Subscript { value, slice } => {
if let PyExpr::Name(id) = value.as_ref() {
if id == "Optional" || id == "Union" || (short_literals && id == "Literal") {
return unparse_pep_604(id, slice, short_literals);
}
}
let mut out = unparse_frags(value, short_literals)?;
out.push(desc_sig_punctuation("["));
out.extend(unparse_frags(slice, short_literals)?);
out.push(desc_sig_punctuation("]"));
let is_literal = matches!(
out[0].text.as_deref(),
Some("Literal") | Some("typing.Literal")
);
if is_literal {
for node in &mut out[1..] {
if node.kind == kinds::TEXT {
let mut wrapper = Node::elem(kinds::LITERAL, Span::ZERO);
wrapper.children.push(std::mem::replace(
node,
Node::elem(kinds::LITERAL, Span::ZERO),
));
*node = wrapper;
}
}
}
Ok(out)
}
PyExpr::UnaryOp { op, operand } => {
let punct = match op {
PyUnaryOp::Invert => desc_sig_punctuation("~"),
PyUnaryOp::USub => desc_sig_punctuation("-"),
PyUnaryOp::UAdd | PyUnaryOp::Not => return Err(Unsupported),
};
let mut out = vec![punct];
out.extend(unparse_frags(operand, short_literals)?);
Ok(out)
}
PyExpr::Tuple(elts) => {
if elts.is_empty() {
Ok(vec![desc_sig_punctuation("("), desc_sig_punctuation(")")])
} else {
let mut out = Vec::new();
join_frags(elts, short_literals, &mut out)?;
Ok(out)
}
}
PyExpr::Call { func, args, kwargs } => {
let mut out = unparse_frags(func, short_literals)?;
out.push(desc_sig_punctuation("("));
let mut inner = Vec::new();
join_frags(args, short_literals, &mut inner)?;
for (name, value) in kwargs {
if !inner.is_empty() {
inner.push(desc_sig_punctuation(","));
inner.push(desc_sig_space());
}
inner.push(desc_sig_name(name));
inner.push(desc_sig_operator("="));
inner.extend(unparse_frags(value, short_literals)?);
}
out.extend(inner);
out.push(desc_sig_punctuation(")"));
Ok(out)
}
PyExpr::Set(_) | PyExpr::Dict(_) => Err(Unsupported),
}
}
fn unparse_pep_604(
value_id: &str,
slice: &PyExpr,
short_literals: bool,
) -> Result<Vec<Node>, Unsupported> {
let mut out = Vec::new();
match slice {
PyExpr::Tuple(elts) => {
let (first, rest) = elts.split_first().ok_or(Unsupported)?;
out.extend(unparse_frags(first, short_literals)?);
for elt in rest {
bitor_frags(&mut out);
out.extend(unparse_frags(elt, short_literals)?);
}
}
other => out.extend(unparse_frags(other, short_literals)?),
}
if value_id == "Optional" {
bitor_frags(&mut out);
out.push(text_frag("None"));
}
Ok(out)
}
fn sig_leaf(kind: &'static str, class: &str, text: &str) -> Node {
let mut node = Node::elem(kind, Span::ZERO);
node.attrs.classes.push(class.to_string());
node.children.push(Node::text_node(text, Span::ZERO));
node
}
pub(crate) fn desc_sig_space() -> Node {
sig_leaf("desc_sig_space", "w", " ")
}
pub(crate) fn desc_sig_name(text: &str) -> Node {
sig_leaf("desc_sig_name", "n", text)
}
pub(crate) fn desc_sig_operator(text: &str) -> Node {
sig_leaf("desc_sig_operator", "o", text)
}
pub(crate) fn desc_sig_punctuation(text: &str) -> Node {
sig_leaf("desc_sig_punctuation", "p", text)
}
pub(crate) fn desc_sig_keyword(text: &str) -> Node {
sig_leaf("desc_sig_keyword", "k", text)
}
pub(crate) fn desc_sig_literal_number(text: &str) -> Node {
sig_leaf("desc_sig_literal_number", "m", text)
}
pub(crate) fn desc_sig_literal_string(text: &str) -> Node {
sig_leaf("desc_sig_literal_string", "s", text)
}
#[cfg(test)]
mod tests {
use super::*;
fn wrap(kind: &'static str, children: Vec<Node>) -> String {
let mut parent = Node::elem(kind, Span::ZERO);
parent.set("xml:space", AttrValue::Str("preserve".to_string()));
parent.children = children;
parent.pformat()
}
fn returns(annotation: &str) -> String {
returns_with(annotation, &PySigConfig::default())
}
fn returns_with(annotation: &str, cfg: &PySigConfig) -> String {
wrap(
"desc_returns",
parse_annotation(annotation, &PyRefContext::default(), cfg),
)
}
fn type_option(annotation: &str) -> String {
let mut children = vec![desc_sig_punctuation(":"), desc_sig_space()];
children.extend(parse_annotation(
annotation,
&PyRefContext::default(),
&PySigConfig::default(),
));
wrap("desc_annotation", children)
}
fn unqualified() -> PySigConfig {
PySigConfig {
python_use_unqualified_type_names: true,
..PySigConfig::default()
}
}
fn short_literals() -> PySigConfig {
PySigConfig {
python_display_short_literal_types: true,
..PySigConfig::default()
}
}
#[test]
fn parse_reftarget_plain_name_is_class() {
assert_eq!(
parse_reftarget("pkg.Cls"),
(
"class".to_string(),
"pkg.Cls".to_string(),
"pkg.Cls".to_string(),
false
)
);
assert_eq!(
parse_reftarget("int"),
(
"class".to_string(),
"int".to_string(),
"int".to_string(),
false
)
);
}
#[test]
fn parse_reftarget_leading_dot_sets_refspecific() {
assert_eq!(
parse_reftarget(".MyClass"),
(
"class".to_string(),
"MyClass".to_string(),
"MyClass".to_string(),
true
)
);
}
#[test]
fn parse_reftarget_tilde_title_is_last_component() {
assert_eq!(
parse_reftarget("~pkg.Cls"),
(
"class".to_string(),
"pkg.Cls".to_string(),
"Cls".to_string(),
false
)
);
}
#[test]
fn parse_reftarget_none_and_typing_targets_are_obj() {
assert_eq!(
parse_reftarget("typing.Any"),
(
"obj".to_string(),
"typing.Any".to_string(),
"Any".to_string(),
false
)
);
assert_eq!(
parse_reftarget("None"),
(
"obj".to_string(),
"None".to_string(),
"None".to_string(),
false
)
);
}
#[test]
fn a_union_renders_xref_space_pipe_space_xref() {
assert_eq!(
returns("int | None"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" |\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
" None\n",
)
);
}
#[test]
fn optional_rewrites_to_pep_604_with_obj_none() {
assert_eq!(returns("Optional[int]"), returns("int | None"));
}
#[test]
fn union_subscript_rewrites_to_pipes() {
assert_eq!(
returns("Union[int, str]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" |\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
)
);
}
#[test]
fn optional_of_union_flattens_and_appends_none() {
assert_eq!(
returns("Optional[Union[int, str]]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" |\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" |\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
" None\n",
)
);
}
#[test]
fn a_subscript_renders_value_bracket_slice_bracket() {
assert_eq!(
returns("list[str]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"list\" reftype=\"class\">\n",
" list\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn a_tuple_slice_joins_with_comma_and_space() {
assert_eq!(
returns("dict[str, int]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"dict\" reftype=\"class\">\n",
" dict\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn nested_subscripts_recurse_flat() {
assert_eq!(
returns("dict[str, list[int]]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"dict\" reftype=\"class\">\n",
" dict\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"list\" reftype=\"class\">\n",
" list\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn a_list_display_renders_punctuation_brackets() {
assert_eq!(
returns("Callable[[int, str], bool]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Callable\" reftype=\"class\">\n",
" Callable\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"bool\" reftype=\"class\">\n",
" bool\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn an_empty_tuple_slice_renders_paren_pair() {
assert_eq!(
returns("Tuple[()]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Tuple\" reftype=\"class\">\n",
" Tuple\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <desc_sig_punctuation classes=\"p\">\n",
" (\n",
" <desc_sig_punctuation classes=\"p\">\n",
" )\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn literal_members_stay_literal_strings_next_to_a_literal_xref() {
assert_eq!(
returns("Literal['a', 'b']"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Literal\" reftype=\"class\">\n",
" Literal\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'a'\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'b'\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn a_none_member_of_literal_stays_bare_text() {
assert_eq!(
returns("Literal[None]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Literal\" reftype=\"class\">\n",
" Literal\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" None\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn typing_literal_is_obj_with_stripped_title() {
assert_eq!(
returns("typing.Literal['a']"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Literal\" reftype=\"obj\">\n",
" Literal\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'a'\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn a_tilde_before_a_name_suppresses_the_title_prefix() {
assert_eq!(
returns("~pkg.Cls"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"pkg.Cls\" reftype=\"class\">\n",
" Cls\n",
)
);
assert_eq!(
returns("~Cls"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Cls\" reftype=\"class\">\n",
" Cls\n",
)
);
}
#[test]
fn typing_prefix_yields_obj_reftype() {
assert_eq!(
returns("typing.Any"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Any\" reftype=\"obj\">\n",
" Any\n",
)
);
}
#[test]
fn bare_none_annotation_is_an_obj_xref() {
assert_eq!(
returns("None"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
" None\n",
)
);
}
#[test]
fn ellipsis_renders_punctuation() {
assert_eq!(
returns("..."),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ...\n",
)
);
}
#[test]
fn true_renders_keyword() {
assert_eq!(
returns("True"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_keyword classes=\"k\">\n",
" True\n",
)
);
}
#[test]
fn an_int_renders_literal_number() {
assert_eq!(
returns("42"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_literal_number classes=\"m\">\n",
" 42\n",
)
);
}
#[test]
fn a_negative_int_renders_minus_punctuation_then_number() {
assert_eq!(
returns("-1"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_punctuation classes=\"p\">\n",
" -\n",
" <desc_sig_literal_number classes=\"m\">\n",
" 1\n",
)
);
}
#[test]
fn a_string_annotation_stays_literal_string_never_an_xref() {
assert_eq!(
returns("'MyClass'"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'MyClass'\n",
)
);
}
#[test]
fn a_u_prefixed_string_drops_the_prefix_like_repr() {
assert_eq!(
returns("u'x'"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'x'\n",
)
);
}
#[test]
fn float_and_bytes_constants_become_xrefs_via_repr_text() {
assert_eq!(
returns("1.5"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"1.5\" reftype=\"class\">\n",
" 1.5\n",
)
);
assert_eq!(
returns("b'x'"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"b'x'\" reftype=\"class\">\n",
" b'x'\n",
)
);
}
#[test]
fn a_call_renders_args_and_keywords() {
assert_eq!(
returns("Annotated[str, Validator(str, len=10)]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Annotated\" reftype=\"class\">\n",
" Annotated\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Validator\" reftype=\"class\">\n",
" Validator\n",
" <desc_sig_punctuation classes=\"p\">\n",
" (\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"str\" reftype=\"class\">\n",
" str\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_name classes=\"n\">\n",
" len\n",
" <desc_sig_operator classes=\"o\">\n",
" =\n",
" <desc_sig_literal_number classes=\"m\">\n",
" 10\n",
" <desc_sig_punctuation classes=\"p\">\n",
" )\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn an_attribute_of_a_subscript_keeps_only_the_first_fragment() {
assert_eq!(
type_option("list[int].x"),
concat!(
"<desc_annotation xml:space=\"preserve\">\n",
" <desc_sig_punctuation classes=\"p\">\n",
" :\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"list.x\" reftype=\"class\">\n",
" list.x\n",
)
);
}
#[test]
fn a_syntax_error_falls_back_to_one_xref_of_the_whole_text() {
assert_eq!(
type_option("List[int"),
concat!(
"<desc_annotation xml:space=\"preserve\">\n",
" <desc_sig_punctuation classes=\"p\">\n",
" :\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"List[int\" reftype=\"class\">\n",
" List[int\n",
)
);
}
#[test]
fn a_leading_dot_falls_back_and_sets_refspecific() {
assert_eq!(
type_option(".MyClass"),
concat!(
"<desc_annotation xml:space=\"preserve\">\n",
" <desc_sig_punctuation classes=\"p\">\n",
" :\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"1\" reftarget=\"MyClass\" reftype=\"class\">\n",
" MyClass\n",
)
);
}
#[test]
fn unsupported_node_shapes_fall_back_to_one_xref() {
assert_eq!(
returns("X + Y"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"X + Y\" reftype=\"class\">\n",
" X + Y\n",
)
);
assert_eq!(
returns("{1, 2}"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"{1, 2}\" reftype=\"class\">\n",
" {1, 2}\n",
)
);
}
#[test]
fn ref_context_lands_in_py_module_and_py_class_attrs() {
let ctx = PyRefContext {
module: Some("mymod".to_string()),
class_: Some("C".to_string()),
span: Span::ZERO,
};
assert_eq!(
type_to_xref("int", &ctx, &PySigConfig::default()).pformat(),
concat!(
"<pending_xref py:class=\"C\" py:module=\"mymod\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
)
);
}
#[test]
fn unqualified_config_emits_condition_pair() {
assert_eq!(
returns_with("pkg.Cls", &unqualified()),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"pkg.Cls\" reftype=\"class\">\n",
" <pending_xref_condition condition=\"resolved\">\n",
" Cls\n",
" <pending_xref_condition condition=\"*\">\n",
" pkg.Cls\n",
)
);
}
#[test]
fn unqualified_tilde_conditions_share_the_short_title() {
assert_eq!(
returns_with("~pkg.mod.Cls", &unqualified()),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"pkg.mod.Cls\" reftype=\"class\">\n",
" <pending_xref_condition condition=\"resolved\">\n",
" Cls\n",
" <pending_xref_condition condition=\"*\">\n",
" Cls\n",
)
);
}
#[test]
fn unqualified_typing_conditions_share_the_stripped_title() {
assert_eq!(
returns_with("typing.Any", &unqualified()),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Any\" reftype=\"obj\">\n",
" <pending_xref_condition condition=\"resolved\">\n",
" Any\n",
" <pending_xref_condition condition=\"*\">\n",
" Any\n",
)
);
}
#[test]
fn short_literal_types_render_pipe_chain_without_literal_xref() {
assert_eq!(
returns_with("Literal['a', 'b']", &short_literals()),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'a'\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" |\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'b'\n",
)
);
}
#[test]
fn a_short_literal_none_member_becomes_an_obj_xref() {
assert_eq!(
returns_with("Literal[1, 'a', None]", &short_literals()),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_literal_number classes=\"m\">\n",
" 1\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" |\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'a'\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" |\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"None\" reftype=\"obj\">\n",
" None\n",
)
);
}
#[test]
fn short_literal_config_ignores_typing_literal() {
assert_eq!(
returns_with("typing.Literal['a', 'b']", &short_literals()),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"typing.Literal\" reftype=\"obj\">\n",
" Literal\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'a'\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_literal_string classes=\"s\">\n",
" 'b'\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn pep_646_star_annotation_splits_the_operator() {
assert_eq!(
returns("*Ts"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_operator classes=\"o\">\n",
" *\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"Ts\" reftype=\"class\">\n",
" Ts\n",
)
);
}
#[test]
fn pep_646_star_annotation_over_a_subscript() {
assert_eq!(
returns("*tuple[int, ...]"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_operator classes=\"o\">\n",
" *\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"tuple\" reftype=\"class\">\n",
" tuple\n",
" <desc_sig_punctuation classes=\"p\">\n",
" [\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"int\" reftype=\"class\">\n",
" int\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <desc_sig_punctuation classes=\"p\">\n",
" ...\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ]\n",
)
);
}
#[test]
fn exec_mode_renders_a_bare_starred_tuple() {
assert_eq!(
returns("*a, b"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <desc_sig_operator classes=\"o\">\n",
" *\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"a\" reftype=\"class\">\n",
" a\n",
" <desc_sig_punctuation classes=\"p\">\n",
" ,\n",
" <desc_sig_space classes=\"w\">\n",
" \n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"b\" reftype=\"class\">\n",
" b\n",
)
);
}
#[test]
fn leading_indent_keeps_the_unstripped_text() {
assert_eq!(
returns(" int"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\" int\" reftype=\"class\">\n",
" int\n",
)
);
}
#[test]
fn an_empty_annotation_renders_no_nodes() {
for text in ["", " ", " ", "\n", "\t"] {
assert!(
parse_annotation(text, &PyRefContext::default(), &PySigConfig::default())
.is_empty(),
"{text:?} must render no nodes"
);
}
}
#[test]
fn a_boolop_annotation_falls_back_to_one_xref() {
assert_eq!(
returns("a or b"),
concat!(
"<desc_returns xml:space=\"preserve\">\n",
" <pending_xref py:class=\"True\" py:module=\"True\" refdomain=\"py\" refspecific=\"0\" reftarget=\"a or b\" reftype=\"class\">\n",
" a or b\n",
)
);
}
}