Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Individual release notes live in [`changelog/`](changelog/).

| Version | Date | Summary |
|---------|------|---------|
| [v0.0.342](changelog/0.0.342.md) | 2026-04-29 | explicit `super(C, inst)` + metaclass `__instancecheck__`/`__subclasscheck__` -- `Class.Metaclass`, fixture 342 |
| [v0.0.341](changelog/0.0.341.md) | 2026-04-29 | encoding handlers + `bytes.hex(sep, group)` -- `str.encode` honors `errors=`, `namereplace`, proper UnicodeEncodeError/DecodeError, fixture 341 |
| [v0.0.340](changelog/0.0.340.md) | 2026-04-29 | exception API -- `add_note`/`__notes__`, `with_traceback`, `__suppress_context__`, BaseExceptionGroup `split`/`subgroup`/`derive`, fixture 340 |
| [v0.0.339](changelog/0.0.339.md) | 2026-04-29 | async generators -- `__aiter__`/`__anext__`/`asend`/`athrow`/`aclose`, async for + comprehension, fixture 339 |
Expand Down
39 changes: 39 additions & 0 deletions changelog/0.0.342.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# v0.0.342 — 2026-04-29

Explicit `super(C, instance)` + metaclass `__instancecheck__` /
`__subclasscheck__`

Closes gaps **B7** and **C3** from the v0.1.0 audit
(`notes/Spec/1500/1530_goipy_v01_gap_audit.md`). Spec at
`notes/Spec/1500/1536_goipy_v0042_super_metaclass.md`.

- `object/super.go` (new): `Super{StartCls, Self}` proxy.
- `object/object.go`: `Class.Metaclass` field — set when a class
body uses `metaclass=` or inherits one from a base.
- `vm/builtins.go`: rewrote the `super` builtin. Two-arg form
`super(C, inst)` returns a `*object.Super`. Zero-arg form
reconstructs `(__class__, self)` from the calling frame's free
cells + `Fast[0]` — needed when CPython's compiler emits
`LOAD_GLOBAL super; CALL 0` instead of the optimized
`LOAD_SUPER_ATTR` (happens when the module also calls
`super(C, inst)` explicitly somewhere).
- `vm/builtins.go` `__build_class__`: honors the `metaclass=`
kwarg and inherits `Metaclass` from the most-derived base.
- `vm/builtins.go` `isinstance` / `issubclass`: prepend a
`metaInstanceCheck` / `metaSubclassCheck` step that, when a
metaclass exposes `__instancecheck__` / `__subclasscheck__`,
invokes it with `(cls, target)` and returns the truthy result.
Existing MRO + ABC paths are unchanged when no metaclass is set.
- `vm/ops.go` `getAttr`: `*object.Super` branch walks
`lookupAfter(typeOf(self), startCls, name)` and binds via
`bindDescriptor`. Mirrors what `LOAD_SUPER_ATTR` does inline.
- New fixture `internal/testdata/342_super_metaclass.py` —
super(B,b).hi() walking past B; three-deep MRO via explicit
super; diamond inheritance with mixed implicit/explicit super;
classmethod super; metaclass `__instancecheck__` and
`__subclasscheck__`; ABCMeta-style virtual subclass via
predicate; metaclass inherited via base.

Out of scope: full metatype protocol (custom `__call__` /
`__new__` on the metaclass), 1-arg unbound `super()` descriptor
protocol, `type(cls)` returning the metaclass instance.
35 changes: 35 additions & 0 deletions internal/testdata/342_super_metaclass.expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# section 1: super(B, b).hi() walks past B to A.hi
B.hi
A.hi
# section 2: three-deep MRO
C.hi
B.hi
A.hi
# section 3: diamond — explicit super matches implicit
G.hi/E.hi/F.hi/D.hi
E.hi/F.hi/D.hi
F.hi/D.hi
# section 4: super on classmethod still binds class
I:I
I
# section 5: metaclass __instancecheck__ overrides isinstance
True
True
True
# section 6: metaclass __subclasscheck__ overrides issubclass
True
True
# section 7: ABCMeta-style virtual subclass via predicate
True
False
False
# section 8: metaclass inherited via base class
True
True
False
# section 9: no metaclass → regular semantics still hold
True
True
False
True
False
179 changes: 179 additions & 0 deletions internal/testdata/342_super_metaclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# PEP-related: explicit super(C, instance) + metaclass __instancecheck__/__subclasscheck__.
# Section markers help diff against CPython.

print("# section 1: super(B, b).hi() walks past B to A.hi")


class A:
def hi(self):
return "A.hi"


class B(A):
def hi(self):
return "B.hi"


b = B()
print(b.hi())
print(super(B, b).hi())


print("# section 2: three-deep MRO")


class C(B):
def hi(self):
return "C.hi"


c = C()
print(c.hi())
print(super(C, c).hi())
print(super(B, c).hi())


print("# section 3: diamond — explicit super matches implicit")


class D:
def hi(self):
return "D.hi"


class E(D):
def hi(self):
return "E.hi" + "/" + super().hi()


class F(D):
def hi(self):
return "F.hi" + "/" + super().hi()


class G(E, F):
def hi(self):
return "G.hi" + "/" + super().hi()


g = G()
print(g.hi())
print(super(G, g).hi())
print(super(E, g).hi())


print("# section 4: super on classmethod still binds class")


class H:
@classmethod
def klass(cls):
return cls.__name__


class I(H):
@classmethod
def klass(cls):
return "I:" + super().klass()


print(I.klass())
print(super(I, I).klass())


print("# section 5: metaclass __instancecheck__ overrides isinstance")


class Meta(type):
def __instancecheck__(cls, inst):
return True


class Q(metaclass=Meta):
pass


print(isinstance([], Q))
print(isinstance(42, Q))
print(isinstance("x", Q))


print("# section 6: metaclass __subclasscheck__ overrides issubclass")


class Meta2(type):
def __subclasscheck__(cls, sub):
return True


class R(metaclass=Meta2):
pass


print(issubclass(int, R))
print(issubclass(list, R))


print("# section 7: ABCMeta-style virtual subclass via predicate")


class DuckMeta(type):
def __instancecheck__(cls, inst):
return hasattr(inst, "quack")


class Duck(metaclass=DuckMeta):
pass


class Mallard:
def quack(self):
return "quack"


class NotADuck:
pass


print(isinstance(Mallard(), Duck))
print(isinstance(NotADuck(), Duck))
print(isinstance(42, Duck))


print("# section 8: metaclass inherited via base class")


class Meta3(type):
def __instancecheck__(cls, inst):
return inst == "magic"


class S(metaclass=Meta3):
pass


class T(S):
pass


print(isinstance("magic", S))
print(isinstance("magic", T))
print(isinstance("nope", T))


print("# section 9: no metaclass → regular semantics still hold")


class U:
pass


class V(U):
pass


print(isinstance(V(), U))
print(isinstance(V(), V))
print(isinstance(42, U))
print(issubclass(V, U))
print(issubclass(U, V))
6 changes: 6 additions & 0 deletions object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,12 @@ type Class struct {
// EnumData is non-nil for enum subclasses (Color, Number, etc.).
// It holds the member list, value map, and iteration order.
EnumData *EnumData

// Metaclass is the explicit metaclass declared via `class C(metaclass=M):`,
// or nil for the default `type` metaclass. When non-nil, isinstance() and
// issubclass() consult Metaclass.__instancecheck__ /
// __subclasscheck__ before the normal MRO walk.
Metaclass *Class
}

// MethodCacheEntry stores one cached classLookup result.
Expand Down
10 changes: 10 additions & 0 deletions object/super.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package object

// Super represents a `super(StartCls, Self)` proxy. Attribute access on a
// Super walks the MRO of `Self`'s class starting strictly after StartCls.
// Methods returned by the lookup are bound to Self so calling looks like a
// regular bound-method call.
type Super struct {
StartCls *Class
Self Object
}
Loading