Skip to content

Commit a6fadd3

Browse files
committed
Preserve explicit Clinic headers for legacy native signatures
Expose the deque and timezone class headers, deque.index optional groups, and type.__prepare__ receiver spelling through existing static documentation. Preserve typed argument binding, documentation bodies and generated defaults for ordinary prose. Assisted-by: OpenAI Codex:model version not exposed
1 parent dd8c343 commit a6fadd3

5 files changed

Lines changed: 137 additions & 0 deletions

File tree

‎crates/stdlib/src/_datetime.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,10 @@ mod _datetime {
24452445

24462446
// timezone
24472447

2448+
/// timezone(offset, name=<unrepresentable>)
2449+
/// --
2450+
///
2451+
/// Fixed offset from UTC implementation of tzinfo.
24482452
#[pyattr]
24492453
#[pyclass(module = "datetime", name = "timezone", base = PyTzInfo, traverse)]
24502454
#[derive(Debug)]

‎crates/vm/src/builtins/type.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3303,6 +3303,10 @@ impl Py<PyType> {
33033303
Ok(())
33043304
}
33053305

3306+
/// __prepare__($cls, name, bases, /, **kwds)
3307+
/// --
3308+
///
3309+
/// Create the namespace for the class statement
33063310
#[pyclassmethod]
33073311
fn __prepare__(_cls: PyTypeRef, _args: PrepareArgs, vm: &VirtualMachine) -> PyDictRef {
33083312
vm.ctx.new_dict()

‎crates/vm/src/function/signature.rs‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,43 @@ const fn write_signature_prefix(buf: &mut [u8], name: &str, args: &[SigArg]) ->
441441
put_str(buf, st.n, ")\n--\n\n")
442442
}
443443

444+
// Explicit Clinic headers cover legacy signatures that are not Python parameter
445+
// lists. Recognize only a complete header, not a call example in prose.
446+
const fn has_internal_doc_signature(name: &str, doc: &str) -> bool {
447+
let name = name.as_bytes();
448+
let doc = doc.as_bytes();
449+
if doc.len() <= name.len() || doc[name.len()] != b'(' {
450+
return false;
451+
}
452+
let mut i = 0;
453+
while i < name.len() {
454+
if doc[i] != name[i] {
455+
return false;
456+
}
457+
i += 1;
458+
}
459+
while i + 5 < doc.len() {
460+
if doc[i] == b')'
461+
&& doc[i + 1] == b'\n'
462+
&& doc[i + 2] == b'-'
463+
&& doc[i + 3] == b'-'
464+
&& doc[i + 4] == b'\n'
465+
&& doc[i + 5] == b'\n'
466+
{
467+
return true;
468+
}
469+
if doc[i] == b'\n' && doc[i + 1] == b'\n' {
470+
return false;
471+
}
472+
i += 1;
473+
}
474+
false
475+
}
476+
444477
const fn write_internal_doc(buf: &mut [u8], name: &str, args: &[SigArg], doc: &str) -> usize {
478+
if has_internal_doc_signature(name, doc) {
479+
return put_str(buf, 0, doc);
480+
}
445481
let n = write_signature_prefix(buf, name, args);
446482
put_str(buf, n, doc)
447483
}
@@ -930,6 +966,40 @@ mod tests {
930966
assert_eq!(rendered(DefaultRepr::Unrepresentable), "<unrepresentable>");
931967
}
932968

969+
#[test]
970+
fn explicit_internal_doc_preserves_legacy_signatures() {
971+
const DOC: &str = "index($self, value, [start, [stop]])\n--\n\nFind a value.";
972+
const ARGS: &[SigArg] = &[SigArg::from_arg::<crate::PyObjectRef>("value")];
973+
const N: usize = super::internal_doc_len("index", ARGS, DOC);
974+
const TEXT: [u8; N] = super::internal_doc_bytes::<N>("index", ARGS, DOC);
975+
assert_eq!(&TEXT, DOC.as_bytes());
976+
assert_eq!(native_call_flags(ARGS, false), PyMethodFlags::O);
977+
assert!(super::has_internal_doc_signature("f", "f()\n--\n\n"));
978+
assert!(super::has_internal_doc_signature(
979+
"f",
980+
"f($self, value='é',\n other=None)\n--\n\nBody."
981+
));
982+
}
983+
984+
#[test]
985+
fn prose_and_incomplete_headers_keep_generated_signatures() {
986+
for doc in [
987+
"f()",
988+
"f()\n--\n",
989+
"f()\n\nExample.\n--\n\nBody.",
990+
"f(first,\n\nsecond)\n--\n\nBody.",
991+
"g()\n--\n\nBody.",
992+
"ff()\n--\n\nBody.",
993+
" f()\n--\n\nBody.",
994+
"Example: f()\n--\n\nBody.",
995+
] {
996+
assert!(!super::has_internal_doc_signature("f", doc), "{doc:?}");
997+
let mut buf = vec![0; super::internal_doc_len("f", &[], doc)];
998+
super::write_internal_doc(&mut buf, "f", &[], doc);
999+
assert_eq!(buf, format!("f()\n--\n\n{doc}").as_bytes());
1000+
}
1001+
}
1002+
9331003
#[test]
9341004
fn native_call_flags_from_params() {
9351005
const fn arg(params: Option<&'static [Param]>) -> SigArg {

‎crates/vm/src/stdlib/_collections.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ mod _collections {
4545
n: isize,
4646
}
4747

48+
/// deque([iterable[, maxlen]])
49+
/// --
50+
///
51+
/// A list-like sequence optimized for data accesses near its endpoints.
4852
#[pyattr]
4953
#[pyclass(
5054
module = "collections",
@@ -599,6 +603,12 @@ mod _collections {
599603
self._extend(&iterable, true, vm)
600604
}
601605

606+
/// index($self, value, [start, [stop]])
607+
/// --
608+
///
609+
/// Return first index of value.
610+
///
611+
/// Raises ValueError if the value is not present.
602612
#[pymethod]
603613
fn index(
604614
&self,

‎extra_tests/snippets/builtin_signature.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,52 @@ class NewList(list):
362362
else:
363363
assert _hashlib.HASHXOF.update.__text_signature__ == "($self, obj, /)"
364364
assert str(inspect.signature(_hashlib.HASHXOF.update)) == "(self, obj, /)"
365+
366+
# Legacy Clinic headers are useful raw metadata even when their optional groups
367+
# or sentinel defaults cannot be represented by inspect.Signature.
368+
import collections
369+
import datetime
370+
371+
for obj, raw, doc in (
372+
(
373+
collections.deque,
374+
"([iterable[, maxlen]])",
375+
"A list-like sequence optimized for data accesses near its endpoints.",
376+
),
377+
(
378+
collections.deque.index,
379+
"($self, value, [start, [stop]])",
380+
"Return first index of value.\n\n"
381+
"Raises ValueError if the value is not present.",
382+
),
383+
(
384+
collections.deque().index,
385+
"($self, value, [start, [stop]])",
386+
"Return first index of value.\n\n"
387+
"Raises ValueError if the value is not present.",
388+
),
389+
(
390+
datetime.timezone,
391+
"(offset, name=<unrepresentable>)",
392+
"Fixed offset from UTC implementation of tzinfo.",
393+
),
394+
):
395+
assert obj.__text_signature__ == raw
396+
assert obj.__doc__ == doc
397+
try:
398+
inspect.signature(obj)
399+
except ValueError:
400+
pass
401+
else:
402+
raise AssertionError(f"legacy signature should remain unrepresentable: {obj}")
403+
404+
# __prepare__ uses $cls, unlike the $type receiver of most native classmethods.
405+
# The descriptor and its bound form must expose the same header but strip the
406+
# bound receiver only from the latter's inspected signature.
407+
prepare = type.__dict__["__prepare__"]
408+
assert prepare.__text_signature__ == "($cls, name, bases, /, **kwds)"
409+
assert str(inspect.signature(prepare)) == "(cls, name, bases, /, **kwds)"
410+
assert type.__prepare__.__text_signature__ == prepare.__text_signature__
411+
assert str(inspect.signature(type.__prepare__)) == "(name, bases, /, **kwds)"
412+
assert type.__prepare__.__doc__ == "Create the namespace for the class statement"
413+
assert type.__prepare__.__self__ is type

0 commit comments

Comments
 (0)