Skip to content

Conversation

Gankra
Copy link
Contributor

@Gankra Gankra commented Aug 18, 2025

Requires some iteration, but this includes the most tedious part -- threading a new concept of DisplaySettings through every type display impl. Currently it only holds a boolean for multiline, but in the future it could also take other things like "render to markdown" or "here's your base indent if you make a newline".

For types which have exposed display functions I've left the old signature as a compatibility polyfill to avoid having to audit everywhere that prints types right off the bat (notably I originally tried doing multiline functions unconditionally and a ton of things churned that clearly weren't ready for multi-line (diagnostics).

The only real use of this API in this PR is to multiline render function types in hovers, which is the highest impact (see snapshot changes).

Fixes astral-sh/ty#1000

@Gankra Gankra added ty Multi-file analysis & type inference server Related to the LSP server labels Aug 18, 2025
Copy link
Contributor

github-actions bot commented Aug 18, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

Copy link
Contributor

github-actions bot commented Aug 18, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

@Gankra
Copy link
Contributor Author

Gankra commented Aug 18, 2025

I've also added multiline rendering for overloads using a similar scheme to pyright (just render each signature on its own line, don't mention overloads at all).

@Gankra
Copy link
Contributor Author

Gankra commented Aug 18, 2025

pydantic.Field now renders as:

click here for long signature...
(
    default: EllipsisType,
    *,
    alias: str | None = PydanticUndefinedType,
    alias_priority: int | None = PydanticUndefinedType,
    validation_alias: str | AliasPath | AliasChoices | None = PydanticUndefinedType,
    serialization_alias: str | None = PydanticUndefinedType,
    title: str | None = PydanticUndefinedType,
    field_title_generator: ((str, FieldInfo, /) -> str) | None = PydanticUndefinedType,
    description: str | None = PydanticUndefinedType,
    examples: list[Any] | None = PydanticUndefinedType,
    exclude: bool | None = PydanticUndefinedType,
    discriminator: str | Unknown | None = PydanticUndefinedType,
    deprecated: @Todo(Support for `types.UnionType` instances in type expressions) | str | bool | None = PydanticUndefinedType,
    json_schema_extra: @Todo(Support for `typing.TypeAlias`) | ((@Todo(Support for `typing.TypeAlias`), /) -> None) | None = PydanticUndefinedType,
    frozen: bool | None = PydanticUndefinedType,
    validate_default: bool | None = PydanticUndefinedType,
    repr: bool = PydanticUndefinedType,
    init: bool | None = PydanticUndefinedType,
    init_var: bool | None = PydanticUndefinedType,
    kw_only: bool | None = PydanticUndefinedType,
    pattern: str | Pattern[str] | None = PydanticUndefinedType,
    strict: bool | None = PydanticUndefinedType,
    coerce_numbers_to_str: bool | None = PydanticUndefinedType,
    gt: SupportsGt | None = PydanticUndefinedType,
    ge: SupportsGe | None = PydanticUndefinedType,
    lt: SupportsLt | None = PydanticUndefinedType,
    le: SupportsLe | None = PydanticUndefinedType,
    multiple_of: int | float | None = PydanticUndefinedType,
    allow_inf_nan: bool | None = PydanticUndefinedType,
    max_digits: int | None = PydanticUndefinedType,
    decimal_places: int | None = PydanticUndefinedType,
    min_length: int | None = PydanticUndefinedType,
    max_length: int | None = PydanticUndefinedType,
    union_mode: Literal["smart", "left_to_right"] = PydanticUndefinedType,
    fail_fast: bool | None = PydanticUndefinedType,
    **extra: @Todo(`Unpack[]` special form)
) -> Any
(
    default: _T@Field,
    *,
    alias: str | None = PydanticUndefinedType,
    alias_priority: int | None = PydanticUndefinedType,
    validation_alias: str | AliasPath | AliasChoices | None = PydanticUndefinedType,
    serialization_alias: str | None = PydanticUndefinedType,
    title: str | None = PydanticUndefinedType,
    field_title_generator: ((str, FieldInfo, /) -> str) | None = PydanticUndefinedType,
    description: str | None = PydanticUndefinedType,
    examples: list[Any] | None = PydanticUndefinedType,
    exclude: bool | None = PydanticUndefinedType,
    discriminator: str | Unknown | None = PydanticUndefinedType,
    deprecated: @Todo(Support for `types.UnionType` instances in type expressions) | str | bool | None = PydanticUndefinedType,
    json_schema_extra: @Todo(Support for `typing.TypeAlias`) | ((@Todo(Support for `typing.TypeAlias`), /) -> None) | None = PydanticUndefinedType,
    frozen: bool | None = PydanticUndefinedType,
    validate_default: bool | None = PydanticUndefinedType,
    repr: bool = PydanticUndefinedType,
    init: bool | None = PydanticUndefinedType,
    init_var: bool | None = PydanticUndefinedType,
    kw_only: bool | None = PydanticUndefinedType,
    pattern: str | Pattern[str] | None = PydanticUndefinedType,
    strict: bool | None = PydanticUndefinedType,
    coerce_numbers_to_str: bool | None = PydanticUndefinedType,
    gt: SupportsGt | None = PydanticUndefinedType,
    ge: SupportsGe | None = PydanticUndefinedType,
    lt: SupportsLt | None = PydanticUndefinedType,
    le: SupportsLe | None = PydanticUndefinedType,
    multiple_of: int | float | None = PydanticUndefinedType,
    allow_inf_nan: bool | None = PydanticUndefinedType,
    max_digits: int | None = PydanticUndefinedType,
    decimal_places: int | None = PydanticUndefinedType,
    min_length: int | None = PydanticUndefinedType,
    max_length: int | None = PydanticUndefinedType,
    union_mode: Literal["smart", "left_to_right"] = PydanticUndefinedType,
    fail_fast: bool | None = PydanticUndefinedType,
    **extra: @Todo(`Unpack[]` special form)
) -> _T@Field
(
    *,
    default_factory: (() -> _T@Field) | ((dict[str, Any], /) -> _T@Field),
    alias: str | None = PydanticUndefinedType,
    alias_priority: int | None = PydanticUndefinedType,
    validation_alias: str | AliasPath | AliasChoices | None = PydanticUndefinedType,
    serialization_alias: str | None = PydanticUndefinedType,
    title: str | None = PydanticUndefinedType,
    field_title_generator: ((str, FieldInfo, /) -> str) | None = PydanticUndefinedType,
    description: str | None = PydanticUndefinedType,
    examples: list[Any] | None = PydanticUndefinedType,
    exclude: bool | None = PydanticUndefinedType,
    discriminator: str | Unknown | None = PydanticUndefinedType,
    deprecated: @Todo(Support for `types.UnionType` instances in type expressions) | str | bool | None = PydanticUndefinedType,
    json_schema_extra: @Todo(Support for `typing.TypeAlias`) | ((@Todo(Support for `typing.TypeAlias`), /) -> None) | None = PydanticUndefinedType,
    frozen: bool | None = PydanticUndefinedType,
    validate_default: bool | None = PydanticUndefinedType,
    repr: bool = PydanticUndefinedType,
    init: bool | None = PydanticUndefinedType,
    init_var: bool | None = PydanticUndefinedType,
    kw_only: bool | None = PydanticUndefinedType,
    pattern: str | Pattern[str] | None = PydanticUndefinedType,
    strict: bool | None = PydanticUndefinedType,
    coerce_numbers_to_str: bool | None = PydanticUndefinedType,
    gt: SupportsGt | None = PydanticUndefinedType,
    ge: SupportsGe | None = PydanticUndefinedType,
    lt: SupportsLt | None = PydanticUndefinedType,
    le: SupportsLe | None = PydanticUndefinedType,
    multiple_of: int | float | None = PydanticUndefinedType,
    allow_inf_nan: bool | None = PydanticUndefinedType,
    max_digits: int | None = PydanticUndefinedType,
    decimal_places: int | None = PydanticUndefinedType,
    min_length: int | None = PydanticUndefinedType,
    max_length: int | None = PydanticUndefinedType,
    union_mode: Literal["smart", "left_to_right"] = PydanticUndefinedType,
    fail_fast: bool | None = PydanticUndefinedType,
    **extra: @Todo(`Unpack[]` special form)
) -> _T@Field
(
    *,
    alias: str | None = PydanticUndefinedType,
    alias_priority: int | None = PydanticUndefinedType,
    validation_alias: str | AliasPath | AliasChoices | None = PydanticUndefinedType,
    serialization_alias: str | None = PydanticUndefinedType,
    title: str | None = PydanticUndefinedType,
    field_title_generator: ((str, FieldInfo, /) -> str) | None = PydanticUndefinedType,
    description: str | None = PydanticUndefinedType,
    examples: list[Any] | None = PydanticUndefinedType,
    exclude: bool | None = PydanticUndefinedType,
    discriminator: str | Unknown | None = PydanticUndefinedType,
    deprecated: @Todo(Support for `types.UnionType` instances in type expressions) | str | bool | None = PydanticUndefinedType,
    json_schema_extra: @Todo(Support for `typing.TypeAlias`) | ((@Todo(Support for `typing.TypeAlias`), /) -> None) | None = PydanticUndefinedType,
    frozen: bool | None = PydanticUndefinedType,
    validate_default: bool | None = PydanticUndefinedType,
    repr: bool = PydanticUndefinedType,
    init: bool | None = PydanticUndefinedType,
    init_var: bool | None = PydanticUndefinedType,
    kw_only: bool | None = PydanticUndefinedType,
    pattern: str | Pattern[str] | None = PydanticUndefinedType,
    strict: bool | None = PydanticUndefinedType,
    coerce_numbers_to_str: bool | None = PydanticUndefinedType,
    gt: SupportsGt | None = PydanticUndefinedType,
    ge: SupportsGe | None = PydanticUndefinedType,
    lt: SupportsLt | None = PydanticUndefinedType,
    le: SupportsLe | None = PydanticUndefinedType,
    multiple_of: int | float | None = PydanticUndefinedType,
    allow_inf_nan: bool | None = PydanticUndefinedType,
    max_digits: int | None = PydanticUndefinedType,
    decimal_places: int | None = PydanticUndefinedType,
    min_length: int | None = PydanticUndefinedType,
    max_length: int | None = PydanticUndefinedType,
    union_mode: Literal["smart", "left_to_right"] = PydanticUndefinedType,
    fail_fast: bool | None = PydanticUndefinedType,
    **extra: @Todo(`Unpack[]` special form)
) -> Any
!!! abstract "Usage Documentation"
    [Fields](../concepts/fields.md)

Create a field for objects that can be configured.

Used to provide extra information about a field, either for the model schema or complex validation. Some arguments
apply only to number fields (`int`, `float`, `Decimal`) and some apply only to `str`.

Note:
    - Any `_Unset` objects will be replaced by the corresponding value defined in the `_DefaultValues` dictionary. If a key for the `_Unset` object is not found in the `_DefaultValues` dictionary, it will default to `None`

Args:
    default: Default value if the field is not set.
    default_factory: A callable to generate the default value. The callable can either take 0 arguments
        (in which case it is called as is) or a single argument containing the already validated data.
    alias: The name to use for the attribute when validating or serializing by alias.
        This is often used for things like converting between snake and camel case.
    alias_priority: Priority of the alias. This affects whether an alias generator is used.
    validation_alias: Like `alias`, but only affects validation, not serialization.
    serialization_alias: Like `alias`, but only affects serialization, not validation.
    title: Human-readable title.
    field_title_generator: A callable that takes a field name and returns title for it.
    description: Human-readable description.
    examples: Example values for this field.
    exclude: Whether to exclude the field from the model serialization.
    discriminator: Field name or Discriminator for discriminating the type in a tagged union.
    deprecated: A deprecation message, an instance of `warnings.deprecated` or the `typing_extensions.deprecated` backport,
        or a boolean. If `True`, a default deprecation message will be emitted when accessing the field.
    json_schema_extra: A dict or callable to provide extra JSON schema properties.
    frozen: Whether the field is frozen. If true, attempts to change the value on an instance will raise an error.
    validate_default: If `True`, apply validation to the default value every time you create an instance.
        Otherwise, for performance reasons, the default value of the field is trusted and not validated.
    repr: A boolean indicating whether to include the field in the `__repr__` output.
    init: Whether the field should be included in the constructor of the dataclass.
        (Only applies to dataclasses.)
    init_var: Whether the field should _only_ be included in the constructor of the dataclass.
        (Only applies to dataclasses.)
    kw_only: Whether the field should be a keyword-only argument in the constructor of the dataclass.
        (Only applies to dataclasses.)
    coerce_numbers_to_str: Whether to enable coercion of any `Number` type to `str` (not applicable in `strict` mode).
    strict: If `True`, strict validation is applied to the field.
        See [Strict Mode](../concepts/strict_mode.md) for details.
    gt: Greater than. If set, value must be greater than this. Only applicable to numbers.
    ge: Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers.
    lt: Less than. If set, value must be less than this. Only applicable to numbers.
    le: Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers.
    multiple_of: Value must be a multiple of this. Only applicable to numbers.
    min_length: Minimum length for iterables.
    max_length: Maximum length for iterables.
    pattern: Pattern for strings (a regular expression).
    allow_inf_nan: Allow `inf`, `-inf`, `nan`. Only applicable to float and [`Decimal`][decimal.Decimal] numbers.
    max_digits: Maximum number of allow digits for strings.
    decimal_places: Maximum number of decimal places allowed for numbers.
    union_mode: The strategy to apply when validating a union. Can be `smart` (the default), or `left_to_right`.
        See [Union Mode](../concepts/unions.md#union-modes) for details.
    fail_fast: If `True`, validation will stop on the first error. If `False`, all validation errors will be collected.
        This option can be applied only to iterable types (list, tuple, set, and frozenset).
    extra: (Deprecated) Extra fields that will be included in the JSON schema.

        !!! warning Deprecated
            The `extra` kwargs is deprecated. Use `json_schema_extra` instead.

Returns:
    A new [`FieldInfo`][pydantic.fields.FieldInfo]. The return annotation is `Any` so `Field` can be used on
        type-annotated fields without causing a type error.

Which is about on-par with pyright (except for all the TODOs)

@Gankra
Copy link
Contributor Author

Gankra commented Aug 18, 2025

I've also now heavily restricted the places where multiline printing recursively propagates -- in particular it seems pylance basically only ever multi-line prints a function type if it's the top-level type. As such most types now apply self.settings.singleline() to supress multiline printing.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

Awesome!

Comment on lines +207 to +210
def my_func(
a,
b
) -> Unknown
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this is a case where keeping it on one line would be preferable.

@@ -600,9 +762,12 @@ impl DisplaySignature<'_> {

/// Internal method to write signature with the signature writer
fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result {
let multiline = self.settings.multiline && self.parameters.len() > 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this self.parameters.len() > 1 heuristic could probably use some improvement. Ideally it would be based on "how many characters is the rendered display"? That might not be too hard to do, if we approach it as building up a vector of segments, summing their length, and then at the end joining them with the right separator?

@Gankra
Copy link
Contributor Author

Gankra commented Aug 19, 2025

Screenshot 2025-08-18 at 9 44 11 PM

Note that the parameters.len() > 1 heuristic is based on my observation of pylance, which indeed seems to have this simplistic approach (however it also fills in a few more details that make it feel less "empty").

@carljm
Copy link
Contributor

carljm commented Aug 19, 2025

Ah ok! Well that suggests that it's fine to go with this heuristic for now.

Copy link
Member

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

I'm fine with this simple heuristic for now. I think it would be good to add some tests that demonstrate how the rendering looks when nesting multiple complex types.

This code looks very similar to what ruff_formatter supports and we could use it to e.g. avoid breaking a simple f(a, b) signature if it fits into some line length that we pick.

What I don't know is if the formatter is fast enough for the case where we emit many diagnostics, but it probably is.

Here's some docs on the formatter supported IR elements: https://github.com/astral-sh/ruff/blob/7dfde3b929c70b5f5fb9933ef09b8005717a8d85/crates/ruff_formatter/src/builders.rs#L1-L0

@@ -611,12 +776,13 @@ impl DisplaySignature<'_> {
let mut star_added = false;
let mut needs_slash = false;
let mut first = true;
let arg_separator = if multiline { ",\n " } else { ", " };
Copy link
Member

Choose a reason for hiding this comment

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

Does the indent work as expected for nested callable types (where a parameter type is a callable type itself that needs to break over multiple lines)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is avoided by the gratuitous uses of settings.singleline() preventing nested types from being line-wrapped almost anywhere.

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'll add tests to show that)

@MichaReiser
Copy link
Member

Just in case it isn't clear from my previous comment. I don't think we need to explore using ruff's formatter now. But we may want to if we want to do more fancy rendering

@Gankra Gankra enabled auto-merge (squash) August 19, 2025 17:30
@Gankra Gankra merged commit c6dcfe3 into main Aug 19, 2025
37 checks passed
@Gankra Gankra deleted the gankra/func-fmt branch August 19, 2025 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
server Related to the LSP server ty Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Smart formatting of types
3 participants