Skip to content

Conversation

Tpt
Copy link
Contributor

@Tpt Tpt commented Sep 12, 2025

inspect::TypeHint is composed of an "annotation" string and a list of "imports" ("from X import Y" kind)

The type is expected to be built using the macros type_hint!(module, name), type_hint_union!(*args) and type_hint_subscript(main, *args) that take care of maintaining the import list

Introspection data generation is done using the hidden type_hint_json macro to avoid that the proc macros generate too much code

Sadly, outside type_hint these macros can't be converted into const functions because they need to do some concatenation. I introduced type_hint! for consistency, happy to convert it to a const function.

Miscellaneous changes:

  • Rename PyType{Info,Check}::TYPE_INFO into TYPE_HINT
  • Drop redundant PyClassImpl::TYPE_NAME

@Tpt Tpt changed the title Introduce TypeHint struct Introspection: Introduce TypeHint struct Sep 12, 2025
@Tpt Tpt force-pushed the tpt/moduleandtype branch 3 times, most recently from 7d6b267 to 7bb5b56 Compare September 12, 2025 13:24
pub struct TypeHint {
/// The type hint annotation
#[doc(hidden)]
pub annotation: &'static str,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have preferred it to be an enum but sadly I don't see how to serialize it: const functions cannot concatenate and recursive macros are not possible

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the enum definition be? I can see if I can come up with any crazy ideas if I know the preferred form.

Copy link
Contributor Author

@Tpt Tpt Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

enum TypeHint {clashes
   BuiltIn { name: &'static str },
   Member { module: &'static str, name: &'static str },
   Union(&[TypeHint]),
   Subscript { value: TypeHint, parameters: &[TypeHint] },
   Callable { arguments: &[TypeHint], return: TypeHint } // Special case because of the nested [] syntax
}

This way it's easy to ensure the syntax is valid and to do manipulation like introducing aliases in case of name conflicts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think if we split the const fn up into one which (recursively) calculates the final length, and a second which serializes to it with &mut [u8] similar to what I did in the concat.rs module, I think we could probably make the const fn work?

Unfortunately this would require MSRV 1.83. But this is currently an experimental feature, I would personally be ok with having it limited to MSRV 1.83.

/// assert_eq!(T.to_string(), "collections.abc.Sequence");
/// ```
#[macro_export]
macro_rules! type_hint {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better as a const function. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, is there a way to merge all the macros into one? Maybe also with implicit syntax

type_hint!(a.A);  // basic type hint
type_hint!(a.A | b.B);  // union type hint
type_hint!(a.A[b.B]);  // parameterised type hint

we could infer all the imports using the dotted path rules, maybe in complex cases we require users to write the imports?

type_hint!(
   from a import A;
   from b import B;
   A.NestedClass | B
);

or maybe can use mixed syntax somehow

type_hint!(type_hint("a", "A.NestedClass") | b.B);

the "implicit syntax" seems nicest for the 99% of cases which users will write, so while I'm not sure exactly how to tie it all together it seems desirable to me (I think?) to have an intuitive syntax for the common case and then an escape hatch for the awkward ones.

Copy link
Contributor Author

@Tpt Tpt Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks indeed much nicer. I think we need a syntax for composition like in #[derive(FromPyObject)]: we want their to build type hints that is stuff like the union of type hints. So we want to write things like type_hint!(tuple[A::INPUT_TYPE, B::INPUT_TYPE]). But my guess it's mostly for auto-generated code.

What about having two macros:

  • For static type hints like type_hint!(a.A[b.B]); with hopefully support for the from a import A syntax.
  • For composition compose_type_hint!() where elements are all supposed to be other type hints like compose_type_hint!(PyTuple::INPUT_TYPE [ A::INPUT_TYPE, B::INPUT_TYPE ]). But I am not sure it's more readable than the current state.

WDYT?

It might also take into account in this design the type stubs in signatures like signature = (foo: collections.abs.Sequence[int]) -> list[int]. My guess is that we can allow there the same syntax than type_hint! and allow to set the from a import A in #[pymodule]. But it makes name conflict detection more fun...

@Tpt Tpt force-pushed the tpt/moduleandtype branch 4 times, most recently from 1f6118d to 47a4fc1 Compare September 12, 2025 14:08
inspect::TypeHint is composed of an "annotation" string and a list of "imports" ("from X import Y" kind)

The type is expected to be built using the macros `type_hint!(module, name)`, `type_hint_union!(*args)` and `type_hint_subscript(main, *args)` that take care of maintaining the import list

Introspection data generation is done using the hidden type_hint_json macro to avoid that the proc macros generate too much code

Sadly, outside `type_hint` these macros can't be converted into const functions because they need to do some concatenation. I introduced `type_hint!` for consistency, happy to convert it to a const function.

Miscellaneous changes:
- Rename PyType{Info,Check}::TYPE_INFO into TYPE_HINT
- Drop redundant PyClassImpl::TYPE_NAME
@Tpt Tpt force-pushed the tpt/moduleandtype branch from 47a4fc1 to 4ece557 Compare September 12, 2025 14:15
@Tpt Tpt marked this pull request as ready for review September 12, 2025 15:01
Copy link
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks like the right direction for making the type hint definitions reliable wrt import resolution.

I think we can iterate a bit to find the best design for users, I wrote down some ideas.

}};
}

/// Allows to build a [`TypeHint`] that is the subscripted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sentence looks unfinished.

pub struct TypeHint {
/// The type hint annotation
#[doc(hidden)]
pub annotation: &'static str,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would the enum definition be? I can see if I can come up with any crazy ideas if I know the preferred form.

/// assert_eq!(T.to_string(), "collections.abc.Sequence");
/// ```
#[macro_export]
macro_rules! type_hint {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, is there a way to merge all the macros into one? Maybe also with implicit syntax

type_hint!(a.A);  // basic type hint
type_hint!(a.A | b.B);  // union type hint
type_hint!(a.A[b.B]);  // parameterised type hint

we could infer all the imports using the dotted path rules, maybe in complex cases we require users to write the imports?

type_hint!(
   from a import A;
   from b import B;
   A.NestedClass | B
);

or maybe can use mixed syntax somehow

type_hint!(type_hint("a", "A.NestedClass") | b.B);

the "implicit syntax" seems nicest for the 99% of cases which users will write, so while I'm not sure exactly how to tie it all together it seems desirable to me (I think?) to have an intuitive syntax for the common case and then an escape hatch for the awkward ones.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the annotation_stub stuff is still present, should we remove it at the same time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code to extract modules was in the stubs.rs module and got removed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants