Skip to content

[ty] support NewTypes of float and complex#21886

Merged
oconnor663 merged 5 commits intomainfrom
newtype_float
Dec 12, 2025
Merged

[ty] support NewTypes of float and complex#21886
oconnor663 merged 5 commits intomainfrom
newtype_float

Conversation

@oconnor663
Copy link
Contributor

@oconnor663 oconnor663 commented Dec 10, 2025

astral-sh/ty#1818

There is a known-failing test case in this first draft, which is related to a very hacky bit that I'm pretty sure is wrong. Feedback needed :) Comments below.

Second pass: allow the two special-cased unions as NewTypeBase variants, and remove the presumption that the concrete base type is a ClassType.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 10, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@oconnor663 oconnor663 added the ty Multi-file analysis & type inference label Dec 10, 2025
@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 10, 2025

mypy_primer results

Changes were detected when running on open source projects
scikit-build-core (https://github.com/scikit-build/scikit-build-core)
+ src/scikit_build_core/build/wheel.py:98:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 41 diagnostics
+ Found 42 diagnostics

pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
- pandas-stubs/_typing.pyi:1217:16: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
- Found 5119 diagnostics
+ Found 5118 diagnostics

pydantic (https://github.com/pydantic/pydantic)
- pydantic/fields.py:943:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:943:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:983:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:983:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1026:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1026:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1066:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1066:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1109:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1109:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1148:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1148:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1188:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1188:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1567:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, Divergent], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
+ pydantic/fields.py:1567:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`, found `Top[dict[Unknown, Unknown]] | (((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`

No memory usage changes detected ✅

@oconnor663 oconnor663 changed the title [ty] first pass at supporting NewTypes of float and complex [ty] support NewTypes of float and complex Dec 11, 2025
@oconnor663 oconnor663 marked this pull request as ready for review December 11, 2025 01:36
@astral-sh-bot
Copy link

astral-sh-bot bot commented Dec 11, 2025

ecosystem-analyzer results

No diagnostic changes detected ✅
Full report with detailed diff (timing results)

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

Nice, I like the semantics and the implementation here!

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.

I also like these semantics and this implementation; nice work!

}
// If we get here, there is no `ClassType` (because this newtype is cyclic), and we don't
// call `f` at all.
// If we get here, there is no `ClassType` (because this newtype is either float/complex or
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worth explicitly commenting here that mapping base class types is used for normalization and applying type mappings, and neither of these apply to int/float/complex (they are already fully normalized and not generic), so it's fine to ignore them here.

Comment on lines 13908 to 13954
/// Returns true if this union is equivalent to `int | float`, which is what `float` expands
/// into in type position.
pub(crate) fn is_int_float(self, db: &'db dyn Db) -> bool {
let elements = self.elements(db);
if elements.len() != 2 {
return false;
}
let mut has_int = false;
let mut has_float = false;
for element in elements {
if let Type::NominalInstance(nominal) = element
&& let Some(known) = nominal.known_class(db)
{
match known {
KnownClass::Int => has_int = true,
KnownClass::Float => has_float = true,
_ => {}
}
}
}
has_int && has_float
}

/// Returns true if this union is equivalent to `int | float | complex`, which is what
/// `complex` expands into in type position.
pub(crate) fn is_int_float_complex(self, db: &'db dyn Db) -> bool {
let elements = self.elements(db);
if elements.len() != 3 {
return false;
}
let mut has_int = false;
let mut has_float = false;
let mut has_complex = false;
for element in elements {
if let Type::NominalInstance(nominal) = element
&& let Some(known) = nominal.known_class(db)
{
match known {
KnownClass::Int => has_int = true,
KnownClass::Float => has_float = true,
KnownClass::Complex => has_complex = true,
_ => {}
}
}
}
has_int && has_float && has_complex
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a single method which returns an option of an enum, since these are mutually exclusive possibilities that require effectively the same work to check... that would be slightly less redundant and slightly more efficient? But I think it doesn't really matter, this way is totally fine.

@oconnor663 oconnor663 enabled auto-merge (squash) December 12, 2025 00:01
@oconnor663 oconnor663 merged commit ddb7645 into main Dec 12, 2025
41 checks passed
@oconnor663 oconnor663 deleted the newtype_float branch December 12, 2025 00:43
@oconnor663
Copy link
Contributor Author

oconnor663 commented Dec 12, 2025

This auto-merge was ultimately correct, but I'm concerned that it happened before CI was finished running on the last "clippy fix" commit. I guess I don't understand the rules for auto-merge?

Edit: Ah Brent set me straight, this is a settings thing:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants