Skip to content

Python: distinguish type[X] class objects from instances in ty attribution#7933

Merged
knutwannheden merged 1 commit into
mainfrom
distinguish-type-x-class-objects-in-ty-attribution
Jun 7, 2026
Merged

Python: distinguish type[X] class objects from instances in ty attribution#7933
knutwannheden merged 1 commit into
mainfrom
distinguish-type-x-class-objects-in-ty-attribution

Conversation

@knutwannheden

@knutwannheden knutwannheden commented Jun 7, 2026

Copy link
Copy Markdown
Contributor

Motivation

In rewrite-python's ty-based type attribution, a value of type type[X] (a class object) was given the exact same JavaType as an instance of X, so class-vs-instance access was indistinguishable in the LST.

ty-types emits type[X] as a subclassOf descriptor whose base is X. The parser resolved that straight to its base X, so a parameter c: type[M] and a parameter inst: M ended up with the identical JavaType.Class for M. Consequently is_assignable_to("…M", type_of(c)) returned True even though c is a class object, not an M instance.

This breaks any recipe that must tell a class from an instance. It surfaced while validating the Pydantic recipes against pydantic/pydantic-settings: ReplaceModelFieldsInstanceAccess rewrites a deprecated instance access obj.model_fieldstype(obj).model_fields, gated on is_assignable_to("pydantic.main.BaseModel", receiver.type). Because type[X] collapsed to X, it rewrote class access such as

for field_name, field_info in settings_cls.model_fields.items():  # settings_cls: type[BaseSettings]

into type(settings_cls).model_fields — which reaches the metaclass and breaks at runtime.

Examples

class M:
    x: int = 0

def via_type_param(c: type[M]):
    return c            # type[M]  -> class object

def via_instance_param(inst: M):
    return inst         # M        -> instance
receiver resolved type is_assignable_to("M", t) is_assignable_to("type", t)
inst (M) JavaType.Class M True False
c (type[M]) Parameterized(type, [M]) False (was True) True

The wrapped class M stays recoverable via type_parameters[0], so attributes/classmethods reached through the class object still resolve, and the call's declaring type remains M.

Summary

  • type[X] (ty's subclassOf X) now resolves to a JavaType.Parameterized over type with X as its sole type parameter — mirroring how list[X] is modelled — instead of collapsing to X. New helper PythonTypeMapping._make_class_object_type.
  • PEP 747 TypeForm[X] is given the same class-object representation (a TypeForm[X] value is the type X, not an instance of X), keeping the two paths consistent.
  • The declaring-type path (_declaring_type_from_descriptor) is unchanged: it still resolves subclassOf/typeForm through to the underlying class, so a member reached through a type[X] value is attributed to X (where it is declared), not to the type wrapper.
  • is_assignable_to needed no changes: because Parameterized.fully_qualified_name returns the raw base name type, is_assignable_to("M", type[M]) is False and is_assignable_to("type", type[M]) is True out of the box.

Test plan

  • New unit tests (mock descriptors, no CLI) in TestSubclassOfDescriptor: type[M]Parameterized(type, [M]); not assignable to M; assignable to type; instance of M still assignable to M; missing baseUnknown; declaring-type path still resolves to M.

  • Updated TestTypeFormDescriptor / TestTypeFormIntegration to assert the new class-object representation.

  • New end-to-end tests in TestClassObjectTypeAttribution (first-party only, no deps): type[M] param not assignable to M but assignable to type; wrapped class recoverable; classmethod reached via type[M] resolves to M; classmethod cls unchanged.

  • uv run --extra dev pytest tests/python/test_type_attribution.py — 140 passed.

  • uv run --extra dev pytest tests/recipes tests/python/all/tree — 455 passed (no regressions).

  • Independent of the dependency-path (Python: accept a caller-provisioned dependency path for ty type attribution #7930) and multi-file-supertype work; based on main.

…y attribution

A value of type type[X] (a class object) was given the exact same JavaType
as an instance of X, so the LST could not tell class access from instance
access. ty emits this as a subclassOf descriptor (base = X), which the parser
resolved straight to its base X.

Model type[X] (and PEP 747 TypeForm[X]) as a JavaType.Parameterized over
type with X as its sole type parameter, mirroring list[X]. is_assignable_to(X,
type[X]) is now False (raw name is type, not X) while the wrapped X stays
recoverable via type_parameters[0] for attribute/classmethod resolution. The
declaring-type path still resolves through to X so members reached via the
class object remain attributed to X.
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Jun 7, 2026
@knutwannheden knutwannheden changed the title rewrite-python: distinguish type[X] class objects from instances in ty attribution Python: distinguish type[X] class objects from instances in ty attribution Jun 7, 2026
@knutwannheden knutwannheden merged commit d8787ed into main Jun 7, 2026
1 check passed
@knutwannheden knutwannheden deleted the distinguish-type-x-class-objects-in-ty-attribution branch June 7, 2026 11:47
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Jun 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant