Skip to content

Commit dbdf63f

Browse files
committed
Add error input recognition prompts
1 parent 16a2eee commit dbdf63f

File tree

4 files changed

+139
-57
lines changed

4 files changed

+139
-57
lines changed

newsfragments/5288.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Embedded doc mode using HTML comments to fix attribute order issues.

pyo3-macros-backend/src/module.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ pub fn pymodule_module_impl(
116116
let ctx = &Ctx::new(&options.krate, None);
117117
let Ctx { pyo3_path, .. } = ctx;
118118
let doc = get_doc(attrs, None, ctx)?;
119-
let doc = get_doc(attrs, None, ctx)?;
120119
let name = options
121120
.name
122121
.map_or_else(|| ident.unraw(), |name| name.value.0);

pyo3-macros-backend/src/pymethod.rs

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -233,55 +233,65 @@ pub fn gen_py_method(
233233
}
234234
}
235235
// ordinary functions (with some specialties)
236-
(_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def(
237-
cls,
238-
spec,
239-
&spec.get_doc(meth_attrs, ctx)?,
240-
&spec.get_doc(meth_attrs, ctx)?,
241-
None,
242-
ctx,
243-
)?),
244-
(_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def(
245-
cls,
246-
spec,
247-
&spec.get_doc(meth_attrs, ctx)?,
248-
&spec.get_doc(meth_attrs, ctx)?,
249-
Some(quote!(#pyo3_path::ffi::METH_CLASS)),
250-
ctx,
251-
)?),
252-
(_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def(
253-
cls,
254-
spec,
255-
&spec.get_doc(meth_attrs, ctx)?,
256-
&spec.get_doc(meth_attrs, ctx)?,
257-
Some(quote!(#pyo3_path::ffi::METH_STATIC)),
258-
ctx,
259-
)?),
236+
(_, FnType::Fn(_)) => {
237+
let doc = spec.get_doc(meth_attrs, ctx)?;
238+
GeneratedPyMethod::Method(impl_py_method_def(
239+
cls,
240+
spec,
241+
&doc,
242+
None,
243+
ctx,
244+
)?)
245+
},
246+
(_, FnType::FnClass(_)) => {
247+
let doc = spec.get_doc(meth_attrs, ctx)?;
248+
GeneratedPyMethod::Method(impl_py_method_def(
249+
cls,
250+
spec,
251+
&doc,
252+
Some(quote!(#pyo3_path::ffi::METH_CLASS)),
253+
ctx,
254+
)?)
255+
},
256+
(_, FnType::FnStatic) => {
257+
let doc = spec.get_doc(meth_attrs, ctx)?;
258+
GeneratedPyMethod::Method(impl_py_method_def(
259+
cls,
260+
spec,
261+
&doc,
262+
Some(quote!(#pyo3_path::ffi::METH_STATIC)),
263+
ctx,
264+
)?)
265+
},
260266
// special prototypes
261267
(_, FnType::FnNew) | (_, FnType::FnNewClass(_)) => {
262268
GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec, ctx)?)
263269
}
264270

265-
(_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def(
266-
cls,
267-
PropertyType::Function {
268-
self_type,
269-
spec,
270-
doc: spec.get_doc(meth_attrs, ctx)?,
271-
doc: spec.get_doc(meth_attrs, ctx)?,
272-
},
273-
ctx,
274-
)?),
275-
(_, FnType::Setter(self_type)) => GeneratedPyMethod::Method(impl_py_setter_def(
276-
cls,
277-
PropertyType::Function {
278-
self_type,
279-
spec,
280-
doc: spec.get_doc(meth_attrs, ctx)?,
281-
doc: spec.get_doc(meth_attrs, ctx)?,
282-
},
283-
ctx,
284-
)?),
271+
(_, FnType::Getter(self_type)) => {
272+
let doc = spec.get_doc(meth_attrs, ctx)?;
273+
GeneratedPyMethod::Method(impl_py_getter_def(
274+
cls,
275+
PropertyType::Function {
276+
self_type,
277+
spec,
278+
doc,
279+
},
280+
ctx,
281+
)?)
282+
},
283+
(_, FnType::Setter(self_type)) => {
284+
let doc = spec.get_doc(meth_attrs, ctx)?;
285+
GeneratedPyMethod::Method(impl_py_setter_def(
286+
cls,
287+
PropertyType::Function {
288+
self_type,
289+
spec,
290+
doc,
291+
},
292+
ctx,
293+
)?)
294+
},
285295
(_, FnType::FnModule(_)) => {
286296
unreachable!("methods cannot be FnModule")
287297
}
@@ -632,7 +642,6 @@ pub fn impl_py_setter_def(
632642
let Ctx { pyo3_path, .. } = ctx;
633643
let python_name = property_type.null_terminated_python_name(ctx)?;
634644
let doc = property_type.doc(ctx)?;
635-
let doc = property_type.doc(ctx)?;
636645
let mut holders = Holders::new();
637646
let setter_impl = match property_type {
638647
PropertyType::Descriptor {
@@ -821,7 +830,6 @@ pub fn impl_py_getter_def(
821830
let Ctx { pyo3_path, .. } = ctx;
822831
let python_name = property_type.null_terminated_python_name(ctx)?;
823832
let doc = property_type.doc(ctx)?;
824-
let doc = property_type.doc(ctx)?;
825833

826834
let mut cfg_attrs = TokenStream::new();
827835
if let PropertyType::Descriptor { field, .. } = &property_type {

pyo3-macros-backend/src/utils.rs

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ pub fn get_doc(
148148
attrs: &mut Vec<syn::Attribute>,
149149
mut text_signature: Option<String>,
150150
ctx: &Ctx,
151-
) -> syn::Result<PythonDoc> {
152151
) -> syn::Result<PythonDoc> {
153152
let Ctx { pyo3_path, .. } = ctx;
154153
// insert special divider between `__text_signature__` and doc
@@ -160,6 +159,7 @@ pub fn get_doc(
160159
let mut parts = Punctuated::<TokenStream, Token![,]>::new();
161160
let mut first = true;
162161
let mut current_part = text_signature.unwrap_or_default();
162+
let mut current_part_span = None; // Track span for error reporting
163163

164164
let mut mode = DocParseMode::Both;
165165

@@ -176,6 +176,11 @@ pub fn get_doc(
176176
..
177177
}) = &nv.value
178178
{
179+
// Update span for error reporting
180+
if current_part_span.is_none() {
181+
current_part_span = Some(lit_str.span());
182+
}
183+
179184
// Strip single left space from literal strings, if needed.
180185
let doc_line = lit_str.value();
181186
let stripped_line = doc_line.strip_prefix(' ').unwrap_or(&doc_line);
@@ -190,11 +195,25 @@ pub fn get_doc(
190195
"python" => DocParseMode::PythonOnly,
191196
"rust" => DocParseMode::RustOnly,
192197
"both" => DocParseMode::Both,
193-
_ => return Err(syn::Error::new(lit_str.span(), format!("Invalid doc_mode: '{}'. Expected 'python', 'rust', or 'both'.", value))),
198+
_ => return Err(syn::Error::new(
199+
lit_str.span(),
200+
format!("Invalid doc_mode: '{}'. Expected 'python', 'rust', or 'both'.", value)
201+
)),
194202
};
195203
// Do not retain mode switch lines in Rust, and skip in Python
196204
continue;
205+
} else if is_likely_pyo3_doc_mode_typo(content_trimmed) {
206+
// Handle potential typos in pyo3_doc_mode prefix
207+
return Err(syn::Error::new(
208+
lit_str.span(),
209+
format!(
210+
"Suspicious comment '{}' - did you mean 'pyo3_doc_mode'? Valid format: <!-- pyo3_doc_mode: python/rust/both -->",
211+
content_trimmed
212+
)
213+
));
197214
}
215+
// If it's an HTML comment but not pyo3_doc_mode related,
216+
// it will be included based on current mode (no special handling)
198217
}
199218

200219
// Not a mode switch, decide based on current mode
@@ -219,8 +238,10 @@ pub fn get_doc(
219238
// Include in Python doc if needed
220239
if include_in_python {
221240
// Reset the string buffer, write that part, and then push this macro part too.
222-
parts.push(current_part.to_token_stream());
223-
current_part.clear();
241+
if !current_part.is_empty() {
242+
parts.push(current_part.to_token_stream());
243+
current_part.clear();
244+
}
224245
parts.push(nv.value.to_token_stream());
225246
}
226247
}
@@ -241,7 +262,10 @@ pub fn get_doc(
241262

242263
// Check if mode ended in Both; if not, error to enforce "pairing"
243264
if !matches!(mode, DocParseMode::Both) {
244-
return Err(err_spanned!(Span::call_site() => "doc_mode did not end in 'both' mode; consider adding <!-- pyo3_doc_mode: both --> at the end"));
265+
return Err(syn::Error::new(
266+
Span::call_site(),
267+
"doc_mode did not end in 'both' mode; consider adding <!-- pyo3_doc_mode: both --> at the end"
268+
));
245269
}
246270

247271
if !parts.is_empty() {
@@ -256,16 +280,13 @@ pub fn get_doc(
256280

257281
syn::Ident::new("concat", Span::call_site()).to_tokens(&mut tokens);
258282
syn::token::Not(Span::call_site()).to_tokens(&mut tokens);
259-
syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| {
283+
syn::token::Paren(Span::call_site()).surround(&mut tokens, |tokens| {
260284
parts.to_tokens(tokens);
261-
syn::token::Comma(Span::call_site()).to_tokens(tokens);
262285
});
263286

264-
Ok(PythonDoc(PythonDocKind::Tokens(
265287
Ok(PythonDoc(PythonDocKind::Tokens(
266288
quote!(#pyo3_path::ffi::c_str!(#tokens)),
267289
)))
268-
)))
269290
} else {
270291
// Just a string doc - return directly with nul terminator
271292
let docs = CString::new(current_part).unwrap();
@@ -274,10 +295,63 @@ pub fn get_doc(
274295
current_part_span.unwrap_or(Span::call_site()),
275296
ctx,
276297
))))
277-
))))
278298
}
279299
}
280300

301+
/// Helper function to detect likely typos in pyo3_doc_mode prefix
302+
fn is_likely_pyo3_doc_mode_typo(content: &str) -> bool {
303+
// Simple fuzzy matching for common typos
304+
let potential_typos = [
305+
"pyo3_doc_mde",
306+
"pyo3_docc_mode",
307+
"pyo3_doc_mod",
308+
"py03_doc_mode",
309+
"pyo3doc_mode",
310+
"pyo3_docmode",
311+
"pyo_doc_mode",
312+
"pyo3_doc_node",
313+
];
314+
315+
potential_typos.iter().any(|&typo| {
316+
content.starts_with(typo) ||
317+
(content.len() >= typo.len() - 2 &&
318+
simple_edit_distance(content.split(':').next().unwrap_or(""), typo) <= 2)
319+
})
320+
}
321+
322+
/// Simple edit distance calculation for typo detection
323+
fn simple_edit_distance(a: &str, b: &str) -> usize {
324+
let a_chars: Vec<char> = a.chars().collect();
325+
let b_chars: Vec<char> = b.chars().collect();
326+
let a_len = a_chars.len();
327+
let b_len = b_chars.len();
328+
329+
if a_len == 0 { return b_len; }
330+
if b_len == 0 { return a_len; }
331+
332+
let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];
333+
334+
// Initialize first row and column
335+
for i in 0..=a_len {
336+
matrix[i][0] = i;
337+
}
338+
for j in 0..=b_len {
339+
matrix[0][j] = j;
340+
}
341+
342+
// Fill the matrix
343+
for i in 1..=a_len {
344+
for j in 1..=b_len {
345+
let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 };
346+
matrix[i][j] = (matrix[i - 1][j] + 1) // deletion
347+
.min(matrix[i][j - 1] + 1) // insertion
348+
.min(matrix[i - 1][j - 1] + cost); // substitution
349+
}
350+
}
351+
352+
matrix[a_len][b_len]
353+
}
354+
281355
impl quote::ToTokens for PythonDoc {
282356
fn to_tokens(&self, tokens: &mut TokenStream) {
283357
match &self.0 {

0 commit comments

Comments
 (0)