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
10 changes: 10 additions & 0 deletions builtins/breakpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ func importBreakpointModule(modulepath string) (objects.Object, error) {
//
// CPython: Python/sysmodule.c:658 sys_breakpointhook warn label
func breakpointWarn(envar string) (objects.Object, error) {
// The import or attribute lookup that brought us here raised a Python
// exception (ModuleNotFoundError / AttributeError) which gopy swallows
// in favor of a warning. CPython's sys_breakpointhook calls PyErr_Clear
// before warning, so clear the lingering thread exception too; otherwise
// a later contextmanager exit or except handler observes the stale error.
//
// CPython: Python/sysmodule.c:658 sys_breakpointhook (PyErr_Clear before warn)
if objects.ClearCurrentExceptionHook != nil {
objects.ClearCurrentExceptionHook()
}
message := fmt.Sprintf("Ignoring unimportable $PYTHONBREAKPOINT: %q", envar)
if err := emitRuntimeWarning(message); err != nil {
return nil, err
Expand Down
9 changes: 9 additions & 0 deletions builtins/ctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1319,6 +1319,15 @@ func bytesIntContents(arg objects.Object) (buf []byte, handled bool, err error)
iv, ierr := objects.NumberIndex(arg)
if ierr != nil {
if isTypeError(ierr) {
// CPython clears the pending TypeError before falling through
// to the buffer / iterable path; without this the swallowed
// exception lingers on the thread and a later `with` exit or
// except handler observes it.
//
// CPython: Objects/bytesobject.c:2812 bytes_new_impl (PyErr_Clear)
if objects.ClearCurrentExceptionHook != nil {
objects.ClearCurrentExceptionHook()
}
return nil, false, nil
}
return nil, true, ierr
Expand Down
31 changes: 31 additions & 0 deletions imp/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,29 @@ type interpState struct {
//
// CPython: Include/internal/pycore_interp.h imports.modules
hiddenExt map[string]objects.Object
// configRestores rolls back the per-interpreter mutable config a
// subinterpreter is allowed to change independently (e.g.
// int_max_str_digits, which CPython keeps in interp->long_state).
// gopy stores those values in package globals, so each registered
// snapshotter captures the parent value on push and PopSubinterp
// restores it, mirroring a real subinterpreter's own copy.
//
// CPython: Include/internal/pycore_interp.h long_state.max_str_digits
configRestores []func()
}

// subinterpSnapshotters hold the per-interpreter config capture callbacks
// other packages register at init time. Each returns a restore closure
// invoked when the subinterpreter is popped.
var subinterpSnapshotters []func() func()

// RegisterSubinterpSnapshot registers a callback that captures a piece of
// mutable interpreter-global config when a subinterpreter is pushed and
// returns a closure that restores it on pop. Used by module/sys to give
// int_max_str_digits per-interpreter semantics without threading the value
// through the import package.
func RegisterSubinterpSnapshot(fn func() func()) {
subinterpSnapshotters = append(subinterpSnapshotters, fn)
}

var (
Expand Down Expand Up @@ -274,6 +297,9 @@ func PushSubinterp(ownGil, checkMulti bool) {
modByIndex: map[int]*objects.Module{},
hiddenExt: hideExtModules(),
}
for _, snap := range subinterpSnapshotters {
s.configRestores = append(s.configRestores, snap())
}
interpMu.Lock()
nextInterpID++
s.id = nextInterpID
Expand All @@ -293,6 +319,11 @@ func PopSubinterp() {
interpMu.Unlock()
if popped != nil {
restoreExtModules(popped.hiddenExt)
// Restore in reverse registration order so nested captures unwind
// symmetrically.
for i := len(popped.configRestores) - 1; i >= 0; i-- {
popped.configRestores[i]()
}
}
}

Expand Down
25 changes: 25 additions & 0 deletions module/_testcapi/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ func buildModule() (*objects.Module, error) {
{"bad_get", badGet},
{"set_nomemory", setNomemory},
{"remove_mem_hooks", removeMemHooks},
{"getbuffer_with_null_view", getbufferWithNullView},
{"config_get", configGet},
{"config_getint", configGetint},
{"config_names", configNames},
Expand Down Expand Up @@ -395,6 +396,30 @@ func removeMemHooks(_ []objects.Object, _ map[string]objects.Object) (objects.Ob
return objects.None(), nil
}

// getbufferWithNullView ports getbuffer_with_null_view: it calls the
// buffer protocol with a NULL view pointer (PyObject_GetBuffer(obj, NULL,
// PyBUF_SIMPLE)). A compliant getbufferproc rejects the obsolete NULL view;
// bytearray's raises BufferError. Objects that do not export a buffer raise
// the TypeError PyObject_GetBuffer would.
//
// CPython: Modules/_testcapimodule.c:1136 getbuffer_with_null_view
func getbufferWithNullView(args []objects.Object, _ map[string]objects.Object) (objects.Object, error) {
if len(args) != 1 {
return nil, fmt.Errorf("TypeError: getbuffer_with_null_view expected 1 argument, got %d", len(args))
}
obj := args[0]
if _, ok := obj.(*objects.ByteArray); ok {
// CPython: Objects/bytearrayobject.c:51 bytearray_getbuffer (view==NULL)
return nil, fmt.Errorf("BufferError: bytearray_getbuffer: view==NULL argument is obsolete")
}
if _, ok := objects.AsBytesLike(obj); ok {
// Any other buffer exporter would dereference the NULL view; the
// NULL-view argument is obsolete, so report it the same way.
return nil, fmt.Errorf("BufferError: view==NULL argument is obsolete")
}
return nil, fmt.Errorf("TypeError: a bytes-like object is required, not '%s'", obj.Type().Name)
}

// fastcallArgs unpacks a tuple-or-None argument into a positional slice
// and count, mirroring fastcall_args.
//
Expand Down
28 changes: 28 additions & 0 deletions module/_testinternalcapi/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func buildModule() (*objects.Module, error) {
{"get_recursion_depth", getRecursionDepth},
{"run_in_subinterp_with_config", runInSubinterpWithConfig},
{"clear_extension", clearExtension},
{"dict_getitem_knownhash", dictGetitemKnownhash},
}
for _, f := range fns {
if err := d.SetItem(objects.NewStr(f.name), objects.NewBuiltinFunction(f.name, f.fn)); err != nil {
Expand Down Expand Up @@ -119,6 +120,33 @@ func runInSubinterpWithConfig(args []objects.Object, _ map[string]objects.Object
return objects.NewInt(int64(builtins.RunInFreshNamespace(code.Value()))), nil
}

// dictGetitemKnownhash ports dict_getitem_knownhash(mp, key, hash): it
// looks up key in mp with the caller-supplied hash. A non-dict first
// argument is a SystemError (the C code passes a non-dict to
// _PyDict_GetItem_KnownHash, which trips its assert/bad-internal-call);
// a missing key reports KeyError(key); a __eq__ that raises propagates.
//
// CPython: Modules/_testinternalcapi.c:1562 dict_getitem_knownhash
func dictGetitemKnownhash(args []objects.Object, _ map[string]objects.Object) (objects.Object, error) {
if len(args) != 3 {
return nil, fmt.Errorf("TypeError: dict_getitem_knownhash expected 3 arguments, got %d", len(args))
}
d, ok := args[0].(*objects.Dict)
if !ok {
return nil, fmt.Errorf("SystemError: bad argument to internal function")
}
h, err := objects.NumberIndex(args[2])
if err != nil {
return nil, err
}
hi, ok := h.(*objects.Int)
if !ok {
return nil, fmt.Errorf("SystemError: bad argument to internal function")
}
hv, _ := hi.Int64()
return d.GetItemKnownHashOrKeyError(args[1], hv)
}

// clearExtension ports clear_extension(name, filename): it clears all
// internally cached data for a single-phase extension module so the test
// suite can re-import it fresh. It delegates to _PyImport_ClearExtension.
Expand Down
13 changes: 13 additions & 0 deletions module/sys/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync/atomic"

"github.com/tamnd/gopy/errors"
"github.com/tamnd/gopy/imp"
"github.com/tamnd/gopy/objects"
pegen "github.com/tamnd/gopy/parser/pegen"
"github.com/tamnd/gopy/state"
Expand Down Expand Up @@ -51,6 +52,18 @@ func init() {
// CPython: Objects/longobject.c:2049 long_to_decimal_string_internal,
// Objects/longobject.c:2943 long_from_string_base
objects.IntMaxStrDigitsHook = intMaxStrDigits.Load
// CPython keeps int_max_str_digits per interpreter
// (interp->long_state.max_str_digits), so a subinterpreter that calls
// sys.set_int_max_str_digits gets its own copy and the parent's value is
// untouched. gopy parks the ceiling in a package global, so register a
// snapshot/restore pair: capture the parent value when a subinterpreter
// is pushed and roll it back when it is popped.
//
// CPython: Include/internal/pycore_interp.h long_state.max_str_digits
imp.RegisterSubinterpSnapshot(func() func() {
saved := intMaxStrDigits.Load()
return func() { intMaxStrDigits.Store(saved) }
})
}

// Bind stamps the runtime helpers onto d: exit, setrecursionlimit,
Expand Down
18 changes: 18 additions & 0 deletions objects/dict.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,24 @@ func (d *Dict) GetItemKnownHash(key Object, h int64) (Object, error) {
return d.slotValue(idx), nil
}

// GetItemKnownHashOrKeyError is GetItemKnownHash with the C-API miss
// contract: a key that is absent (and no comparison raised) reports
// KeyError(key) rather than the internal not-found sentinel. A raised
// __eq__ propagates unchanged. This backs _testinternalcapi's
// dict_getitem_knownhash probe.
//
// CPython: Objects/dictobject.c:1965 _PyDict_GetItem_KnownHash
func (d *Dict) GetItemKnownHashOrKeyError(key Object, h int64) (Object, error) {
v, err := d.GetItemKnownHash(key, h)
if err != nil {
if errors.Is(err, errKeyNotFound) {
return nil, raiseKeyError(key)
}
return nil, err
}
return v, nil
}

// ContainsKnownHash is Contains with a caller-supplied hash.
//
// CPython: Objects/dictobject.c:2530 _PyDict_Contains_KnownHash
Expand Down
40 changes: 31 additions & 9 deletions objects/usertype.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func NewUserTypeMetaE(name string, bases []*Type, ns *Dict, kwargs map[string]Ob
stampMetaclass(t, meta)
installSubclassAttrSlots(t)
noSlotsDeclared := hasNoSlotsDeclared(ns)
configureManagedDict(t, bases, noSlotsDeclared)
needDictDescr, needWeakrefDescr := configureManagedDict(t, bases, noSlotsDeclared)
// type_new_set_attrs copies the namespace into tp_dict (slots, classcell,
// plain attributes) BEFORE type_ready -> mro_internal invokes the
// metaclass mro(). A metaclass that overrides mro() can therefore read
Expand All @@ -302,6 +302,20 @@ func NewUserTypeMetaE(name string, bases []*Type, ns *Dict, kwargs map[string]Ob
if err := processClassNamespace(t, ns); err != nil {
return nil, err
}
// type_new_descriptors stamps the __dict__ and __weakref__ getsets AFTER
// the namespace was copied into tp_dict, so they sort after the user's
// class-body names in dict order (bpo-34320 test_namespace_order). The
// add uses PyDict_SetDefaultRef, so a class body that already binds
// __dict__ or __weakref__ (e.g. `__dict__ = property(...)`) keeps its own
// value rather than being overwritten by the managed-dict getset.
//
// CPython: Objects/typeobject.c:8136 type_add_getset (PyDict_SetDefaultRef)
if needDictDescr && !nsHasName(ns, "__dict__") {
installInstanceDictDescr(t)
}
if needWeakrefDescr && !nsHasName(ns, "__weakref__") {
installInstanceWeakrefDescr(t)
}
if err := applyMetaclassMRO(t, meta); err != nil {
return nil, err
}
Expand Down Expand Up @@ -593,7 +607,18 @@ func hasNoSlotsDeclared(ns *Dict) bool {
// CPython: Objects/typeobject.c:4153 type_new (sets
// Py_TPFLAGS_INLINE_VALUES + Py_TPFLAGS_MANAGED_DICT on heap types with
// a managed dict)
func configureManagedDict(t *Type, bases []*Type, noSlotsDeclared bool) {
// nsHasName reports whether the class-body namespace bound name, the
// signal type_new_descriptors reads (via PyDict_SetDefaultRef) to leave a
// user-provided __dict__ / __weakref__ untouched.
func nsHasName(ns *Dict, name string) bool {
if ns == nil {
return false
}
has, _ := ns.Contains(NewStr(name))
return has
}

func configureManagedDict(t *Type, bases []*Type, noSlotsDeclared bool) (needDictDescr, needWeakrefDescr bool) {
inheritedDict := false
for _, b := range bases {
if b != nil && b.HasDict {
Expand All @@ -611,9 +636,7 @@ func configureManagedDict(t *Type, bases []*Type, noSlotsDeclared bool) {
// `class C: pass` but not for a subclass of C.
//
// CPython: Objects/typeobject.c type_new_descriptors (add_dict gate)
if t.HasDict && !inheritedDict {
installInstanceDictDescr(t)
}
needDictDescr = t.HasDict && !inheritedDict
// HasWeakref tracks tp_weaklistoffset. It inherits from any base that
// provides weak-reference support, and the no-__slots__ case adds it
// for the new type whenever the solid base is not a variable-size
Expand All @@ -640,16 +663,15 @@ func configureManagedDict(t *Type, bases []*Type, noSlotsDeclared bool) {
// already sees it through the MRO.
//
// CPython: Objects/typeobject.c type_new_descriptors (add_weak gate)
if t.HasWeakref && !inheritedWeakref {
installInstanceWeakrefDescr(t)
}
needWeakrefDescr = t.HasWeakref && !inheritedWeakref
if !t.HasDict {
return
return needDictDescr, needWeakrefDescr
}
t.TpFlags |= TpFlagManagedDict
if basesAllowInlineValues(bases, noSlotsDeclared) {
t.TpFlags |= TpFlagInlineValues
}
return needDictDescr, needWeakrefDescr
}

// mroHasDict reports whether any class in b's MRO carries a per-instance
Expand Down
8 changes: 4 additions & 4 deletions test/cpython/MANIFEST.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,14 @@ test_traceback done v0.12.7 370/370 green
test_abstract_numbers ready v0.4.0 7/7 pass
test_bool ready v0.2.0 31/31 pass
test_buffer out-of-scope - C-level buffer protocol API
test_builtin ready v0.7.0 147/147 pass (skipped=6)
test_bytes ready v0.4.0 317/317 pass (skipped=8)
test_builtin ready v0.13.6 133/133 pass (skipped=7); 1726 bridge: breakpoint PyErr_Clear + type-creation namespace order
test_bytes ready v0.13.6 317/317 pass (skipped=9); 1726 bridge: getbuffer_with_null_view ported
test_complex ready v0.4.0 37/37 pass
test_dict ready v0.2.0 120/120 pass
test_dict ready v0.13.6 118/120 pass (skipped=1); 1726 bridge: dict_getitem_knownhash ported. 2 documented impl-detail residuals: test_splittable_popitem (PEP 412 split tables), test_oob_indexing_dictiter_iternextitem (borrow-model iterator, spec 1727)
test_dictviews ready v0.2.0 16/16 pass
test_float ready v0.4.0 54/54 pass
test_funcattrs ready v0.5.0 35/35 pass
test_int ready v0.2.0 52/52 pass
test_int ready v0.13.6 52/52 pass (skipped=7); 1726 bridge: per-interpreter int_max_str_digits snapshot/restore
test_list ready v0.2.0 68/68 pass
test_long ready v0.2.0 47/47 pass
test_memoryview ready v0.4.0 171/171 pass (skipped=18)
Expand Down
Loading