mirror of https://github.com/python/cpython
gh-76785: Drop PyInterpreterID_Type (gh-117101)
I added it quite a while ago as a strategy for managing interpreter lifetimes relative to the PEP 554 (now 734) implementation. Relatively recently I refactored that implementation to no longer rely on InterpreterID objects. Thus now I'm removing it.
This commit is contained in:
parent
abdd1f938f
commit
617158e078
|
@ -1,14 +0,0 @@
|
|||
#ifndef Py_CPYTHON_INTERPRETERIDOBJECT_H
|
||||
# error "this header file must not be included directly"
|
||||
#endif
|
||||
|
||||
/* Interpreter ID Object */
|
||||
|
||||
PyAPI_DATA(PyTypeObject) PyInterpreterID_Type;
|
||||
|
||||
PyAPI_FUNC(PyObject *) PyInterpreterID_New(int64_t);
|
||||
PyAPI_FUNC(PyObject *) PyInterpreterState_GetIDObject(PyInterpreterState *);
|
||||
|
||||
#ifdef Py_BUILD_CORE
|
||||
extern int64_t _PyInterpreterID_GetID(PyObject *);
|
||||
#endif
|
|
@ -295,12 +295,11 @@ _PyInterpreterState_SetFinalizing(PyInterpreterState *interp, PyThreadState *tst
|
|||
}
|
||||
|
||||
|
||||
extern int64_t _PyInterpreterState_ObjectToID(PyObject *);
|
||||
|
||||
// Export for the _xxinterpchannels module.
|
||||
// Exports for the _testinternalcapi module.
|
||||
PyAPI_FUNC(int64_t) _PyInterpreterState_ObjectToID(PyObject *);
|
||||
PyAPI_FUNC(PyInterpreterState *) _PyInterpreterState_LookUpID(int64_t);
|
||||
PyAPI_FUNC(PyInterpreterState *) _PyInterpreterState_LookUpIDObject(PyObject *);
|
||||
|
||||
PyAPI_FUNC(int) _PyInterpreterState_IDInitref(PyInterpreterState *);
|
||||
PyAPI_FUNC(int) _PyInterpreterState_IDIncref(PyInterpreterState *);
|
||||
PyAPI_FUNC(void) _PyInterpreterState_IDDecref(PyInterpreterState *);
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
#ifndef Py_INTERPRETERIDOBJECT_H
|
||||
#define Py_INTERPRETERIDOBJECT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef Py_LIMITED_API
|
||||
# define Py_CPYTHON_INTERPRETERIDOBJECT_H
|
||||
# include "cpython/interpreteridobject.h"
|
||||
# undef Py_CPYTHON_INTERPRETERIDOBJECT_H
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* !Py_INTERPRETERIDOBJECT_H */
|
|
@ -2207,132 +2207,264 @@ class SubinterpreterTest(unittest.TestCase):
|
|||
@requires_subinterpreters
|
||||
class InterpreterIDTests(unittest.TestCase):
|
||||
|
||||
InterpreterID = _testcapi.get_interpreterid_type()
|
||||
|
||||
def new_interpreter(self):
|
||||
def ensure_destroyed(interpid):
|
||||
def add_interp_cleanup(self, interpid):
|
||||
def ensure_destroyed():
|
||||
try:
|
||||
_interpreters.destroy(interpid)
|
||||
except _interpreters.InterpreterNotFoundError:
|
||||
pass
|
||||
self.addCleanup(ensure_destroyed)
|
||||
|
||||
def new_interpreter(self):
|
||||
id = _interpreters.create()
|
||||
self.addCleanup(lambda: ensure_destroyed(id))
|
||||
self.add_interp_cleanup(id)
|
||||
return id
|
||||
|
||||
def test_with_int(self):
|
||||
id = self.InterpreterID(10, force=True)
|
||||
def test_conversion_int(self):
|
||||
convert = _testinternalcapi.normalize_interp_id
|
||||
interpid = convert(10)
|
||||
self.assertEqual(interpid, 10)
|
||||
|
||||
self.assertEqual(int(id), 10)
|
||||
|
||||
def test_coerce_id(self):
|
||||
class Int(str):
|
||||
def test_conversion_coerced(self):
|
||||
convert = _testinternalcapi.normalize_interp_id
|
||||
class MyInt(str):
|
||||
def __index__(self):
|
||||
return 10
|
||||
interpid = convert(MyInt())
|
||||
self.assertEqual(interpid, 10)
|
||||
|
||||
id = self.InterpreterID(Int(), force=True)
|
||||
self.assertEqual(int(id), 10)
|
||||
def test_conversion_from_interpreter(self):
|
||||
convert = _testinternalcapi.normalize_interp_id
|
||||
interpid = self.new_interpreter()
|
||||
converted = convert(interpid)
|
||||
self.assertEqual(converted, interpid)
|
||||
|
||||
def test_conversion_bad(self):
|
||||
convert = _testinternalcapi.normalize_interp_id
|
||||
|
||||
def test_bad_id(self):
|
||||
for badid in [
|
||||
object(),
|
||||
10.0,
|
||||
'10',
|
||||
b'10',
|
||||
]:
|
||||
with self.subTest(badid):
|
||||
with self.subTest(f'bad: {badid!r}'):
|
||||
with self.assertRaises(TypeError):
|
||||
self.InterpreterID(badid)
|
||||
convert(badid)
|
||||
|
||||
badid = -1
|
||||
with self.subTest(badid):
|
||||
with self.subTest(f'bad: {badid!r}'):
|
||||
with self.assertRaises(ValueError):
|
||||
self.InterpreterID(badid)
|
||||
convert(badid)
|
||||
|
||||
badid = 2**64
|
||||
with self.subTest(badid):
|
||||
with self.subTest(f'bad: {badid!r}'):
|
||||
with self.assertRaises(OverflowError):
|
||||
self.InterpreterID(badid)
|
||||
convert(badid)
|
||||
|
||||
def test_exists(self):
|
||||
id = self.new_interpreter()
|
||||
def test_lookup_exists(self):
|
||||
interpid = self.new_interpreter()
|
||||
self.assertTrue(
|
||||
_testinternalcapi.interpreter_exists(interpid))
|
||||
|
||||
def test_lookup_does_not_exist(self):
|
||||
interpid = _testinternalcapi.unused_interpreter_id()
|
||||
self.assertFalse(
|
||||
_testinternalcapi.interpreter_exists(interpid))
|
||||
|
||||
def test_lookup_destroyed(self):
|
||||
interpid = _interpreters.create()
|
||||
_interpreters.destroy(interpid)
|
||||
self.assertFalse(
|
||||
_testinternalcapi.interpreter_exists(interpid))
|
||||
|
||||
def test_linked_lifecycle_does_not_exist(self):
|
||||
exists = _testinternalcapi.interpreter_exists
|
||||
is_linked = _testinternalcapi.interpreter_refcount_linked
|
||||
link = _testinternalcapi.link_interpreter_refcount
|
||||
unlink = _testinternalcapi.unlink_interpreter_refcount
|
||||
get_refcount = _testinternalcapi.get_interpreter_refcount
|
||||
incref = _testinternalcapi.interpreter_incref
|
||||
decref = _testinternalcapi.interpreter_decref
|
||||
|
||||
with self.subTest('never existed'):
|
||||
interpid = _testinternalcapi.unused_interpreter_id()
|
||||
self.assertFalse(
|
||||
exists(interpid))
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
self.InterpreterID(int(id) + 1) # unforced
|
||||
|
||||
def test_does_not_exist(self):
|
||||
id = self.new_interpreter()
|
||||
is_linked(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
self.InterpreterID(int(id) + 1) # unforced
|
||||
|
||||
def test_destroyed(self):
|
||||
id = _interpreters.create()
|
||||
_interpreters.destroy(id)
|
||||
link(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
self.InterpreterID(id) # unforced
|
||||
|
||||
def test_str(self):
|
||||
id = self.InterpreterID(10, force=True)
|
||||
self.assertEqual(str(id), '10')
|
||||
|
||||
def test_repr(self):
|
||||
id = self.InterpreterID(10, force=True)
|
||||
self.assertEqual(repr(id), 'InterpreterID(10)')
|
||||
|
||||
def test_equality(self):
|
||||
id1 = self.new_interpreter()
|
||||
id2 = self.InterpreterID(id1)
|
||||
id3 = self.InterpreterID(
|
||||
self.new_interpreter())
|
||||
|
||||
self.assertTrue(id2 == id2) # identity
|
||||
self.assertTrue(id2 == id1) # int-equivalent
|
||||
self.assertTrue(id1 == id2) # reversed
|
||||
self.assertTrue(id2 == int(id2))
|
||||
self.assertTrue(id2 == float(int(id2)))
|
||||
self.assertTrue(float(int(id2)) == id2)
|
||||
self.assertFalse(id2 == float(int(id2)) + 0.1)
|
||||
self.assertFalse(id2 == str(int(id2)))
|
||||
self.assertFalse(id2 == 2**1000)
|
||||
self.assertFalse(id2 == float('inf'))
|
||||
self.assertFalse(id2 == 'spam')
|
||||
self.assertFalse(id2 == id3)
|
||||
|
||||
self.assertFalse(id2 != id2)
|
||||
self.assertFalse(id2 != id1)
|
||||
self.assertFalse(id1 != id2)
|
||||
self.assertTrue(id2 != id3)
|
||||
|
||||
def test_linked_lifecycle(self):
|
||||
id1 = _interpreters.create()
|
||||
_testinternalcapi.unlink_interpreter_refcount(id1)
|
||||
self.assertEqual(
|
||||
_testinternalcapi.get_interpreter_refcount(id1),
|
||||
0)
|
||||
|
||||
id2 = self.InterpreterID(id1)
|
||||
self.assertEqual(
|
||||
_testinternalcapi.get_interpreter_refcount(id1),
|
||||
1)
|
||||
|
||||
# The interpreter isn't linked to ID objects, so it isn't destroyed.
|
||||
del id2
|
||||
self.assertEqual(
|
||||
_testinternalcapi.get_interpreter_refcount(id1),
|
||||
0)
|
||||
|
||||
_testinternalcapi.link_interpreter_refcount(id1)
|
||||
self.assertEqual(
|
||||
_testinternalcapi.get_interpreter_refcount(id1),
|
||||
0)
|
||||
|
||||
id3 = self.InterpreterID(id1)
|
||||
self.assertEqual(
|
||||
_testinternalcapi.get_interpreter_refcount(id1),
|
||||
1)
|
||||
|
||||
# The interpreter is linked now so is destroyed.
|
||||
del id3
|
||||
unlink(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
_testinternalcapi.get_interpreter_refcount(id1)
|
||||
get_refcount(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
incref(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
decref(interpid)
|
||||
|
||||
with self.subTest('destroyed'):
|
||||
interpid = _interpreters.create()
|
||||
_interpreters.destroy(interpid)
|
||||
self.assertFalse(
|
||||
exists(interpid))
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
is_linked(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
link(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
unlink(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
get_refcount(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
incref(interpid)
|
||||
with self.assertRaises(_interpreters.InterpreterNotFoundError):
|
||||
decref(interpid)
|
||||
|
||||
def test_linked_lifecycle_initial(self):
|
||||
is_linked = _testinternalcapi.interpreter_refcount_linked
|
||||
get_refcount = _testinternalcapi.get_interpreter_refcount
|
||||
|
||||
# A new interpreter will start out not linked, with a refcount of 0.
|
||||
interpid = _testinternalcapi.new_interpreter()
|
||||
self.add_interp_cleanup(interpid)
|
||||
linked = is_linked(interpid)
|
||||
refcount = get_refcount(interpid)
|
||||
|
||||
self.assertFalse(linked)
|
||||
self.assertEqual(refcount, 0)
|
||||
|
||||
def test_linked_lifecycle_never_linked(self):
|
||||
exists = _testinternalcapi.interpreter_exists
|
||||
is_linked = _testinternalcapi.interpreter_refcount_linked
|
||||
get_refcount = _testinternalcapi.get_interpreter_refcount
|
||||
incref = _testinternalcapi.interpreter_incref
|
||||
decref = _testinternalcapi.interpreter_decref
|
||||
|
||||
interpid = _testinternalcapi.new_interpreter()
|
||||
self.add_interp_cleanup(interpid)
|
||||
|
||||
# Incref will not automatically link it.
|
||||
incref(interpid)
|
||||
self.assertFalse(
|
||||
is_linked(interpid))
|
||||
self.assertEqual(
|
||||
1, get_refcount(interpid))
|
||||
|
||||
# It isn't linked so it isn't destroyed.
|
||||
decref(interpid)
|
||||
self.assertTrue(
|
||||
exists(interpid))
|
||||
self.assertFalse(
|
||||
is_linked(interpid))
|
||||
self.assertEqual(
|
||||
0, get_refcount(interpid))
|
||||
|
||||
def test_linked_lifecycle_link_unlink(self):
|
||||
exists = _testinternalcapi.interpreter_exists
|
||||
is_linked = _testinternalcapi.interpreter_refcount_linked
|
||||
link = _testinternalcapi.link_interpreter_refcount
|
||||
unlink = _testinternalcapi.unlink_interpreter_refcount
|
||||
|
||||
interpid = _testinternalcapi.new_interpreter()
|
||||
self.add_interp_cleanup(interpid)
|
||||
|
||||
# Linking at refcount 0 does not destroy the interpreter.
|
||||
link(interpid)
|
||||
self.assertTrue(
|
||||
exists(interpid))
|
||||
self.assertTrue(
|
||||
is_linked(interpid))
|
||||
|
||||
# Unlinking at refcount 0 does not destroy the interpreter.
|
||||
unlink(interpid)
|
||||
self.assertTrue(
|
||||
exists(interpid))
|
||||
self.assertFalse(
|
||||
is_linked(interpid))
|
||||
|
||||
def test_linked_lifecycle_link_incref_decref(self):
|
||||
exists = _testinternalcapi.interpreter_exists
|
||||
is_linked = _testinternalcapi.interpreter_refcount_linked
|
||||
link = _testinternalcapi.link_interpreter_refcount
|
||||
get_refcount = _testinternalcapi.get_interpreter_refcount
|
||||
incref = _testinternalcapi.interpreter_incref
|
||||
decref = _testinternalcapi.interpreter_decref
|
||||
|
||||
interpid = _testinternalcapi.new_interpreter()
|
||||
self.add_interp_cleanup(interpid)
|
||||
|
||||
# Linking it will not change the refcount.
|
||||
link(interpid)
|
||||
self.assertTrue(
|
||||
is_linked(interpid))
|
||||
self.assertEqual(
|
||||
0, get_refcount(interpid))
|
||||
|
||||
# Decref with a refcount of 0 is not allowed.
|
||||
incref(interpid)
|
||||
self.assertEqual(
|
||||
1, get_refcount(interpid))
|
||||
|
||||
# When linked, decref back to 0 destroys the interpreter.
|
||||
decref(interpid)
|
||||
self.assertFalse(
|
||||
exists(interpid))
|
||||
|
||||
def test_linked_lifecycle_incref_link(self):
|
||||
is_linked = _testinternalcapi.interpreter_refcount_linked
|
||||
link = _testinternalcapi.link_interpreter_refcount
|
||||
get_refcount = _testinternalcapi.get_interpreter_refcount
|
||||
incref = _testinternalcapi.interpreter_incref
|
||||
|
||||
interpid = _testinternalcapi.new_interpreter()
|
||||
self.add_interp_cleanup(interpid)
|
||||
|
||||
incref(interpid)
|
||||
self.assertEqual(
|
||||
1, get_refcount(interpid))
|
||||
|
||||
# Linking it will not reset the refcount.
|
||||
link(interpid)
|
||||
self.assertTrue(
|
||||
is_linked(interpid))
|
||||
self.assertEqual(
|
||||
1, get_refcount(interpid))
|
||||
|
||||
def test_linked_lifecycle_link_incref_unlink_decref(self):
|
||||
exists = _testinternalcapi.interpreter_exists
|
||||
is_linked = _testinternalcapi.interpreter_refcount_linked
|
||||
link = _testinternalcapi.link_interpreter_refcount
|
||||
unlink = _testinternalcapi.unlink_interpreter_refcount
|
||||
get_refcount = _testinternalcapi.get_interpreter_refcount
|
||||
incref = _testinternalcapi.interpreter_incref
|
||||
decref = _testinternalcapi.interpreter_decref
|
||||
|
||||
interpid = _testinternalcapi.new_interpreter()
|
||||
self.add_interp_cleanup(interpid)
|
||||
|
||||
link(interpid)
|
||||
self.assertTrue(
|
||||
is_linked(interpid))
|
||||
|
||||
incref(interpid)
|
||||
self.assertEqual(
|
||||
1, get_refcount(interpid))
|
||||
|
||||
# Unlinking it will not change the refcount.
|
||||
unlink(interpid)
|
||||
self.assertFalse(
|
||||
is_linked(interpid))
|
||||
self.assertEqual(
|
||||
1, get_refcount(interpid))
|
||||
|
||||
# Unlinked: decref back to 0 does not destroys the interpreter.
|
||||
decref(interpid)
|
||||
self.assertTrue(
|
||||
exists(interpid))
|
||||
self.assertEqual(
|
||||
0, get_refcount(interpid))
|
||||
|
||||
|
||||
class BuiltinStaticTypesTests(unittest.TestCase):
|
||||
|
|
|
@ -507,7 +507,6 @@ OBJECT_OBJS= \
|
|||
Objects/floatobject.o \
|
||||
Objects/frameobject.o \
|
||||
Objects/funcobject.o \
|
||||
Objects/interpreteridobject.o \
|
||||
Objects/iterobject.o \
|
||||
Objects/listobject.o \
|
||||
Objects/longobject.o \
|
||||
|
@ -1003,7 +1002,6 @@ PYTHON_HEADERS= \
|
|||
$(srcdir)/Include/frameobject.h \
|
||||
$(srcdir)/Include/genericaliasobject.h \
|
||||
$(srcdir)/Include/import.h \
|
||||
$(srcdir)/Include/interpreteridobject.h \
|
||||
$(srcdir)/Include/intrcheck.h \
|
||||
$(srcdir)/Include/iterobject.h \
|
||||
$(srcdir)/Include/listobject.h \
|
||||
|
@ -1077,7 +1075,6 @@ PYTHON_HEADERS= \
|
|||
$(srcdir)/Include/cpython/genobject.h \
|
||||
$(srcdir)/Include/cpython/import.h \
|
||||
$(srcdir)/Include/cpython/initconfig.h \
|
||||
$(srcdir)/Include/cpython/interpreteridobject.h \
|
||||
$(srcdir)/Include/cpython/listobject.h \
|
||||
$(srcdir)/Include/cpython/longintrepr.h \
|
||||
$(srcdir)/Include/cpython/longobject.h \
|
||||
|
|
|
@ -19,3 +19,20 @@ clear_xid_class(PyTypeObject *cls)
|
|||
return _PyCrossInterpreterData_UnregisterClass(cls);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef RETURNS_INTERPID_OBJECT
|
||||
static PyObject *
|
||||
get_interpid_obj(PyInterpreterState *interp)
|
||||
{
|
||||
if (_PyInterpreterState_IDInitref(interp) != 0) {
|
||||
return NULL;
|
||||
};
|
||||
int64_t id = PyInterpreterState_GetID(interp);
|
||||
if (id < 0) {
|
||||
return NULL;
|
||||
}
|
||||
assert(id < LLONG_MAX);
|
||||
return PyLong_FromLongLong(id);
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
#include "_testcapi/parts.h"
|
||||
|
||||
#include "frameobject.h" // PyFrame_New()
|
||||
#include "interpreteridobject.h" // PyInterpreterID_Type
|
||||
#include "marshal.h" // PyMarshal_WriteLongToFile()
|
||||
|
||||
#include <float.h> // FLT_MAX
|
||||
|
@ -1449,12 +1448,6 @@ run_in_subinterp(PyObject *self, PyObject *args)
|
|||
return PyLong_FromLong(r);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
get_interpreterid_type(PyObject *self, PyObject *Py_UNUSED(ignored))
|
||||
{
|
||||
return Py_NewRef(&PyInterpreterID_Type);
|
||||
}
|
||||
|
||||
static PyMethodDef ml;
|
||||
|
||||
static PyObject *
|
||||
|
@ -3299,7 +3292,6 @@ static PyMethodDef TestMethods[] = {
|
|||
{"crash_no_current_thread", crash_no_current_thread, METH_NOARGS},
|
||||
{"test_current_tstate_matches", test_current_tstate_matches, METH_NOARGS},
|
||||
{"run_in_subinterp", run_in_subinterp, METH_VARARGS},
|
||||
{"get_interpreterid_type", get_interpreterid_type, METH_NOARGS},
|
||||
{"create_cfunction", create_cfunction, METH_NOARGS},
|
||||
{"call_in_temporary_c_thread", call_in_temporary_c_thread, METH_VARARGS,
|
||||
PyDoc_STR("set_error_class(error_class) -> None")},
|
||||
|
|
|
@ -1475,6 +1475,83 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||
}
|
||||
|
||||
|
||||
static PyObject *
|
||||
normalize_interp_id(PyObject *self, PyObject *idobj)
|
||||
{
|
||||
int64_t interpid = _PyInterpreterState_ObjectToID(idobj);
|
||||
if (interpid < 0) {
|
||||
return NULL;
|
||||
}
|
||||
return PyLong_FromLongLong(interpid);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
unused_interpreter_id(PyObject *self, PyObject *Py_UNUSED(ignored))
|
||||
{
|
||||
int64_t interpid = INT64_MAX;
|
||||
assert(interpid > _PyRuntime.interpreters.next_id);
|
||||
return PyLong_FromLongLong(interpid);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
new_interpreter(PyObject *self, PyObject *Py_UNUSED(ignored))
|
||||
{
|
||||
// Unlike _interpreters.create(), we do not automatically link
|
||||
// the interpreter to its refcount.
|
||||
PyThreadState *save_tstate = PyThreadState_Get();
|
||||
const PyInterpreterConfig config = \
|
||||
(PyInterpreterConfig)_PyInterpreterConfig_INIT;
|
||||
PyThreadState *tstate = NULL;
|
||||
PyStatus status = Py_NewInterpreterFromConfig(&tstate, &config);
|
||||
PyThreadState_Swap(save_tstate);
|
||||
if (PyStatus_Exception(status)) {
|
||||
_PyErr_SetFromPyStatus(status);
|
||||
return NULL;
|
||||
}
|
||||
PyInterpreterState *interp = PyThreadState_GetInterpreter(tstate);
|
||||
|
||||
if (_PyInterpreterState_IDInitref(interp) < 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
int64_t interpid = PyInterpreterState_GetID(interp);
|
||||
if (interpid < 0) {
|
||||
goto error;
|
||||
}
|
||||
PyObject *idobj = PyLong_FromLongLong(interpid);
|
||||
if (idobj == NULL) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
PyThreadState_Swap(tstate);
|
||||
PyThreadState_Clear(tstate);
|
||||
PyThreadState_Swap(save_tstate);
|
||||
PyThreadState_Delete(tstate);
|
||||
|
||||
return idobj;
|
||||
|
||||
error:
|
||||
save_tstate = PyThreadState_Swap(tstate);
|
||||
Py_EndInterpreter(tstate);
|
||||
PyThreadState_Swap(save_tstate);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpreter_exists(PyObject *self, PyObject *idobj)
|
||||
{
|
||||
PyInterpreterState *interp = _PyInterpreterState_LookUpIDObject(idobj);
|
||||
if (interp == NULL) {
|
||||
if (PyErr_ExceptionMatches(PyExc_InterpreterNotFoundError)) {
|
||||
PyErr_Clear();
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
assert(PyErr_Occurred());
|
||||
return NULL;
|
||||
}
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
get_interpreter_refcount(PyObject *self, PyObject *idobj)
|
||||
{
|
||||
|
@ -1509,6 +1586,41 @@ unlink_interpreter_refcount(PyObject *self, PyObject *idobj)
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpreter_refcount_linked(PyObject *self, PyObject *idobj)
|
||||
{
|
||||
PyInterpreterState *interp = _PyInterpreterState_LookUpIDObject(idobj);
|
||||
if (interp == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (_PyInterpreterState_RequiresIDRef(interp)) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpreter_incref(PyObject *self, PyObject *idobj)
|
||||
{
|
||||
PyInterpreterState *interp = _PyInterpreterState_LookUpIDObject(idobj);
|
||||
if (interp == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
_PyInterpreterState_IDIncref(interp);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpreter_decref(PyObject *self, PyObject *idobj)
|
||||
{
|
||||
PyInterpreterState *interp = _PyInterpreterState_LookUpIDObject(idobj);
|
||||
if (interp == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
_PyInterpreterState_IDDecref(interp);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
||||
static void
|
||||
_xid_capsule_destructor(PyObject *capsule)
|
||||
|
@ -1749,9 +1861,16 @@ static PyMethodDef module_functions[] = {
|
|||
{"run_in_subinterp_with_config",
|
||||
_PyCFunction_CAST(run_in_subinterp_with_config),
|
||||
METH_VARARGS | METH_KEYWORDS},
|
||||
{"normalize_interp_id", normalize_interp_id, METH_O},
|
||||
{"unused_interpreter_id", unused_interpreter_id, METH_NOARGS},
|
||||
{"new_interpreter", new_interpreter, METH_NOARGS},
|
||||
{"interpreter_exists", interpreter_exists, METH_O},
|
||||
{"get_interpreter_refcount", get_interpreter_refcount, METH_O},
|
||||
{"link_interpreter_refcount", link_interpreter_refcount, METH_O},
|
||||
{"unlink_interpreter_refcount", unlink_interpreter_refcount, METH_O},
|
||||
{"interpreter_refcount_linked", interpreter_refcount_linked, METH_O},
|
||||
{"interpreter_incref", interpreter_incref, METH_O},
|
||||
{"interpreter_decref", interpreter_decref, METH_O},
|
||||
{"compile_perf_trampoline_entry", compile_perf_trampoline_entry, METH_VARARGS},
|
||||
{"perf_trampoline_set_persist_after_fork", perf_trampoline_set_persist_after_fork, METH_VARARGS},
|
||||
{"get_crossinterp_data", get_crossinterp_data, METH_VARARGS},
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
#endif
|
||||
|
||||
#include "Python.h"
|
||||
#include "interpreteridobject.h"
|
||||
#include "pycore_crossinterp.h" // struct _xid
|
||||
#include "pycore_interp.h" // _PyInterpreterState_LookUpID()
|
||||
|
||||
|
@ -18,7 +17,9 @@
|
|||
#endif
|
||||
|
||||
#define REGISTERS_HEAP_TYPES
|
||||
#define RETURNS_INTERPID_OBJECT
|
||||
#include "_interpreters_common.h"
|
||||
#undef RETURNS_INTERPID_OBJECT
|
||||
#undef REGISTERS_HEAP_TYPES
|
||||
|
||||
|
||||
|
@ -2908,7 +2909,7 @@ channelsmod_list_interpreters(PyObject *self, PyObject *args, PyObject *kwds)
|
|||
goto except;
|
||||
}
|
||||
if (res) {
|
||||
interpid_obj = PyInterpreterState_GetIDObject(interp);
|
||||
interpid_obj = get_interpid_obj(interp);
|
||||
if (interpid_obj == NULL) {
|
||||
goto except;
|
||||
}
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
#include "pycore_pyerrors.h" // _Py_excinfo
|
||||
#include "pycore_pystate.h" // _PyInterpreterState_SetRunningMain()
|
||||
|
||||
#include "interpreteridobject.h"
|
||||
#include "marshal.h" // PyMarshal_ReadObjectFromString()
|
||||
|
||||
#define RETURNS_INTERPID_OBJECT
|
||||
#include "_interpreters_common.h"
|
||||
#undef RETURNS_INTERPID_OBJECT
|
||||
|
||||
|
||||
#define MODULE_NAME _xxsubinterpreters
|
||||
|
@ -38,20 +39,6 @@ _get_current_interp(void)
|
|||
#define look_up_interp _PyInterpreterState_LookUpIDObject
|
||||
|
||||
|
||||
static PyObject *
|
||||
get_interpid_obj(PyInterpreterState *interp)
|
||||
{
|
||||
if (_PyInterpreterState_IDInitref(interp) != 0) {
|
||||
return NULL;
|
||||
};
|
||||
int64_t id = PyInterpreterState_GetID(interp);
|
||||
if (id < 0) {
|
||||
return NULL;
|
||||
}
|
||||
assert(id < LLONG_MAX);
|
||||
return PyLong_FromLongLong(id);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
_get_current_module(void)
|
||||
{
|
||||
|
|
|
@ -1,274 +0,0 @@
|
|||
/* InterpreterID object */
|
||||
|
||||
#include "Python.h"
|
||||
#include "pycore_interp.h" // _PyInterpreterState_LookUpID()
|
||||
#include "interpreteridobject.h"
|
||||
|
||||
|
||||
typedef struct interpid {
|
||||
PyObject_HEAD
|
||||
int64_t id;
|
||||
} interpid;
|
||||
|
||||
int64_t
|
||||
_PyInterpreterID_GetID(PyObject *self)
|
||||
{
|
||||
if (!PyObject_TypeCheck(self, &PyInterpreterID_Type)) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"expected an InterpreterID, got %R",
|
||||
self);
|
||||
return -1;
|
||||
|
||||
}
|
||||
int64_t id = ((interpid *)self)->id;
|
||||
assert(id >= 0);
|
||||
return id;
|
||||
}
|
||||
|
||||
static interpid *
|
||||
newinterpid(PyTypeObject *cls, int64_t id, int force)
|
||||
{
|
||||
PyInterpreterState *interp = _PyInterpreterState_LookUpID(id);
|
||||
if (interp == NULL) {
|
||||
if (force) {
|
||||
PyErr_Clear();
|
||||
}
|
||||
else {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (interp != NULL) {
|
||||
if (_PyInterpreterState_IDIncref(interp) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
interpid *self = PyObject_New(interpid, cls);
|
||||
if (self == NULL) {
|
||||
if (interp != NULL) {
|
||||
_PyInterpreterState_IDDecref(interp);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
self->id = id;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpid_new(PyTypeObject *cls, PyObject *args, PyObject *kwds)
|
||||
{
|
||||
static char *kwlist[] = {"id", "force", NULL};
|
||||
PyObject *idobj;
|
||||
int force = 0;
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds,
|
||||
"O|$p:InterpreterID.__init__", kwlist,
|
||||
&idobj, &force)) {
|
||||
return NULL;
|
||||
}
|
||||
int64_t id = _PyInterpreterState_ObjectToID(idobj);
|
||||
if (id < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return (PyObject *)newinterpid(cls, id, force);
|
||||
}
|
||||
|
||||
static void
|
||||
interpid_dealloc(PyObject *v)
|
||||
{
|
||||
int64_t id = ((interpid *)v)->id;
|
||||
PyInterpreterState *interp = _PyInterpreterState_LookUpID(id);
|
||||
if (interp != NULL) {
|
||||
_PyInterpreterState_IDDecref(interp);
|
||||
}
|
||||
else {
|
||||
// already deleted
|
||||
PyErr_Clear();
|
||||
}
|
||||
Py_TYPE(v)->tp_free(v);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpid_repr(PyObject *self)
|
||||
{
|
||||
PyTypeObject *type = Py_TYPE(self);
|
||||
const char *name = _PyType_Name(type);
|
||||
interpid *id = (interpid *)self;
|
||||
return PyUnicode_FromFormat("%s(%" PRId64 ")", name, id->id);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpid_str(PyObject *self)
|
||||
{
|
||||
interpid *id = (interpid *)self;
|
||||
return PyUnicode_FromFormat("%" PRId64 "", id->id);
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpid_int(PyObject *self)
|
||||
{
|
||||
interpid *id = (interpid *)self;
|
||||
return PyLong_FromLongLong(id->id);
|
||||
}
|
||||
|
||||
static PyNumberMethods interpid_as_number = {
|
||||
0, /* nb_add */
|
||||
0, /* nb_subtract */
|
||||
0, /* nb_multiply */
|
||||
0, /* nb_remainder */
|
||||
0, /* nb_divmod */
|
||||
0, /* nb_power */
|
||||
0, /* nb_negative */
|
||||
0, /* nb_positive */
|
||||
0, /* nb_absolute */
|
||||
0, /* nb_bool */
|
||||
0, /* nb_invert */
|
||||
0, /* nb_lshift */
|
||||
0, /* nb_rshift */
|
||||
0, /* nb_and */
|
||||
0, /* nb_xor */
|
||||
0, /* nb_or */
|
||||
(unaryfunc)interpid_int, /* nb_int */
|
||||
0, /* nb_reserved */
|
||||
0, /* nb_float */
|
||||
|
||||
0, /* nb_inplace_add */
|
||||
0, /* nb_inplace_subtract */
|
||||
0, /* nb_inplace_multiply */
|
||||
0, /* nb_inplace_remainder */
|
||||
0, /* nb_inplace_power */
|
||||
0, /* nb_inplace_lshift */
|
||||
0, /* nb_inplace_rshift */
|
||||
0, /* nb_inplace_and */
|
||||
0, /* nb_inplace_xor */
|
||||
0, /* nb_inplace_or */
|
||||
|
||||
0, /* nb_floor_divide */
|
||||
0, /* nb_true_divide */
|
||||
0, /* nb_inplace_floor_divide */
|
||||
0, /* nb_inplace_true_divide */
|
||||
|
||||
(unaryfunc)interpid_int, /* nb_index */
|
||||
};
|
||||
|
||||
static Py_hash_t
|
||||
interpid_hash(PyObject *self)
|
||||
{
|
||||
interpid *id = (interpid *)self;
|
||||
PyObject *obj = PyLong_FromLongLong(id->id);
|
||||
if (obj == NULL) {
|
||||
return -1;
|
||||
}
|
||||
Py_hash_t hash = PyObject_Hash(obj);
|
||||
Py_DECREF(obj);
|
||||
return hash;
|
||||
}
|
||||
|
||||
static PyObject *
|
||||
interpid_richcompare(PyObject *self, PyObject *other, int op)
|
||||
{
|
||||
if (op != Py_EQ && op != Py_NE) {
|
||||
Py_RETURN_NOTIMPLEMENTED;
|
||||
}
|
||||
|
||||
if (!PyObject_TypeCheck(self, &PyInterpreterID_Type)) {
|
||||
Py_RETURN_NOTIMPLEMENTED;
|
||||
}
|
||||
|
||||
interpid *id = (interpid *)self;
|
||||
int equal;
|
||||
if (PyObject_TypeCheck(other, &PyInterpreterID_Type)) {
|
||||
interpid *otherid = (interpid *)other;
|
||||
equal = (id->id == otherid->id);
|
||||
}
|
||||
else if (PyLong_CheckExact(other)) {
|
||||
/* Fast path */
|
||||
int overflow;
|
||||
long long otherid = PyLong_AsLongLongAndOverflow(other, &overflow);
|
||||
if (otherid == -1 && PyErr_Occurred()) {
|
||||
return NULL;
|
||||
}
|
||||
equal = !overflow && (otherid >= 0) && (id->id == otherid);
|
||||
}
|
||||
else if (PyNumber_Check(other)) {
|
||||
PyObject *pyid = PyLong_FromLongLong(id->id);
|
||||
if (pyid == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
PyObject *res = PyObject_RichCompare(pyid, other, op);
|
||||
Py_DECREF(pyid);
|
||||
return res;
|
||||
}
|
||||
else {
|
||||
Py_RETURN_NOTIMPLEMENTED;
|
||||
}
|
||||
|
||||
if ((op == Py_EQ && equal) || (op == Py_NE && !equal)) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(interpid_doc,
|
||||
"A interpreter ID identifies a interpreter and may be used as an int.");
|
||||
|
||||
PyTypeObject PyInterpreterID_Type = {
|
||||
PyVarObject_HEAD_INIT(&PyType_Type, 0)
|
||||
"InterpreterID", /* tp_name */
|
||||
sizeof(interpid), /* tp_basicsize */
|
||||
0, /* tp_itemsize */
|
||||
(destructor)interpid_dealloc, /* tp_dealloc */
|
||||
0, /* tp_vectorcall_offset */
|
||||
0, /* tp_getattr */
|
||||
0, /* tp_setattr */
|
||||
0, /* tp_as_async */
|
||||
(reprfunc)interpid_repr, /* tp_repr */
|
||||
&interpid_as_number, /* tp_as_number */
|
||||
0, /* tp_as_sequence */
|
||||
0, /* tp_as_mapping */
|
||||
interpid_hash, /* tp_hash */
|
||||
0, /* tp_call */
|
||||
(reprfunc)interpid_str, /* tp_str */
|
||||
0, /* tp_getattro */
|
||||
0, /* tp_setattro */
|
||||
0, /* tp_as_buffer */
|
||||
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
|
||||
interpid_doc, /* tp_doc */
|
||||
0, /* tp_traverse */
|
||||
0, /* tp_clear */
|
||||
interpid_richcompare, /* tp_richcompare */
|
||||
0, /* tp_weaklistoffset */
|
||||
0, /* tp_iter */
|
||||
0, /* tp_iternext */
|
||||
0, /* tp_methods */
|
||||
0, /* tp_members */
|
||||
0, /* tp_getset */
|
||||
0, /* tp_base */
|
||||
0, /* tp_dict */
|
||||
0, /* tp_descr_get */
|
||||
0, /* tp_descr_set */
|
||||
0, /* tp_dictoffset */
|
||||
0, /* tp_init */
|
||||
0, /* tp_alloc */
|
||||
interpid_new, /* tp_new */
|
||||
};
|
||||
|
||||
PyObject *PyInterpreterID_New(int64_t id)
|
||||
{
|
||||
return (PyObject *)newinterpid(&PyInterpreterID_Type, id, 0);
|
||||
}
|
||||
|
||||
PyObject *
|
||||
PyInterpreterState_GetIDObject(PyInterpreterState *interp)
|
||||
{
|
||||
if (_PyInterpreterState_IDInitref(interp) != 0) {
|
||||
return NULL;
|
||||
};
|
||||
int64_t id = PyInterpreterState_GetID(interp);
|
||||
if (id < 0) {
|
||||
return NULL;
|
||||
}
|
||||
return (PyObject *)newinterpid(&PyInterpreterID_Type, id, 0);
|
||||
}
|
|
@ -24,8 +24,6 @@
|
|||
#include "pycore_typevarobject.h" // _PyTypeAlias_Type, _Py_initialize_generic
|
||||
#include "pycore_unionobject.h" // _PyUnion_Type
|
||||
|
||||
#include "interpreteridobject.h" // _PyInterpreterID_Type
|
||||
|
||||
#ifdef Py_LIMITED_API
|
||||
// Prevent recursive call _Py_IncRef() <=> Py_INCREF()
|
||||
# error "Py_LIMITED_API macro must not be defined"
|
||||
|
@ -2240,7 +2238,6 @@ static PyTypeObject* static_types[] = {
|
|||
&PyGen_Type,
|
||||
&PyGetSetDescr_Type,
|
||||
&PyInstanceMethod_Type,
|
||||
&PyInterpreterID_Type,
|
||||
&PyListIter_Type,
|
||||
&PyListRevIter_Type,
|
||||
&PyList_Type,
|
||||
|
|
|
@ -142,7 +142,6 @@
|
|||
<ClCompile Include="..\Objects\funcobject.c" />
|
||||
<ClCompile Include="..\Objects\genericaliasobject.c" />
|
||||
<ClCompile Include="..\Objects\genobject.c" />
|
||||
<ClCompile Include="..\Objects\interpreteridobject.c" />
|
||||
<ClCompile Include="..\Objects\iterobject.c" />
|
||||
<ClCompile Include="..\Objects\listobject.c" />
|
||||
<ClCompile Include="..\Objects\longobject.c" />
|
||||
|
|
|
@ -241,9 +241,6 @@
|
|||
<ClCompile Include="..\Python\lock.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Objects\interpreteridobject.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\PC\invalid_parameter_handler.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
|
|
@ -154,7 +154,6 @@
|
|||
<ClInclude Include="..\Include\cpython\genobject.h" />
|
||||
<ClInclude Include="..\Include\cpython\import.h" />
|
||||
<ClInclude Include="..\Include\cpython\initconfig.h" />
|
||||
<ClInclude Include="..\Include\cpython\interpreteridobject.h" />
|
||||
<ClInclude Include="..\Include\cpython\listobject.h" />
|
||||
<ClInclude Include="..\Include\cpython\longintrepr.h" />
|
||||
<ClInclude Include="..\Include\cpython\longobject.h" />
|
||||
|
@ -303,7 +302,6 @@
|
|||
<ClInclude Include="..\Include\internal\pycore_unicodeobject_generated.h" />
|
||||
<ClInclude Include="..\Include\internal\pycore_warnings.h" />
|
||||
<ClInclude Include="..\Include\internal\pycore_weakref.h" />
|
||||
<ClInclude Include="..\Include\interpreteridobject.h" />
|
||||
<ClInclude Include="..\Include\intrcheck.h" />
|
||||
<ClInclude Include="..\Include\iterobject.h" />
|
||||
<ClInclude Include="..\Include\listobject.h" />
|
||||
|
@ -504,7 +502,6 @@
|
|||
<ClCompile Include="..\Objects\funcobject.c" />
|
||||
<ClCompile Include="..\Objects\genericaliasobject.c" />
|
||||
<ClCompile Include="..\Objects\genobject.c" />
|
||||
<ClCompile Include="..\Objects\interpreteridobject.c" />
|
||||
<ClCompile Include="..\Objects\iterobject.c" />
|
||||
<ClCompile Include="..\Objects\listobject.c" />
|
||||
<ClCompile Include="..\Objects\longobject.c" />
|
||||
|
|
|
@ -330,9 +330,6 @@
|
|||
<ClInclude Include="..\Include\pyhash.h">
|
||||
<Filter>Include</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Include\interpreteridobject.h">
|
||||
<Filter>Include</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Modules\hashtable.h">
|
||||
<Filter>Modules</Filter>
|
||||
</ClInclude>
|
||||
|
@ -492,9 +489,6 @@
|
|||
<ClInclude Include="..\Include\cpython\genobject.h">
|
||||
<Filter>Include</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Include\cpython\interpreteridobject.h">
|
||||
<Filter>Include\cpython</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\Include\cpython\pythonrun.h">
|
||||
<Filter>Include\cpython</Filter>
|
||||
</ClInclude>
|
||||
|
@ -1475,9 +1469,6 @@
|
|||
<ClCompile Include="..\Objects\namespaceobject.c">
|
||||
<Filter>Objects</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Objects\interpreteridobject.c">
|
||||
<Filter>Objects</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\Modules\_opcode.c">
|
||||
<Filter>Modules</Filter>
|
||||
</ClCompile>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
/* Thread and interpreter state structures and their interfaces */
|
||||
|
||||
#include "Python.h"
|
||||
#include "interpreteridobject.h" // PyInterpreterID_Type
|
||||
#include "pycore_abstract.h" // _PyIndex_Check()
|
||||
#include "pycore_ceval.h"
|
||||
#include "pycore_code.h" // stats
|
||||
|
@ -1131,10 +1130,6 @@ PyInterpreterState_GetDict(PyInterpreterState *interp)
|
|||
int64_t
|
||||
_PyInterpreterState_ObjectToID(PyObject *idobj)
|
||||
{
|
||||
if (PyObject_TypeCheck(idobj, &PyInterpreterID_Type)) {
|
||||
return _PyInterpreterID_GetID(idobj);
|
||||
}
|
||||
|
||||
if (!_PyIndex_Check(idobj)) {
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"interpreter ID must be an int, got %.100s",
|
||||
|
|
|
@ -54,7 +54,6 @@ Objects/genobject.c - _PyAsyncGenASend_Type -
|
|||
Objects/genobject.c - _PyAsyncGenAThrow_Type -
|
||||
Objects/genobject.c - _PyAsyncGenWrappedValue_Type -
|
||||
Objects/genobject.c - _PyCoroWrapper_Type -
|
||||
Objects/interpreteridobject.c - PyInterpreterID_Type -
|
||||
Objects/iterobject.c - PyCallIter_Type -
|
||||
Objects/iterobject.c - PySeqIter_Type -
|
||||
Objects/iterobject.c - _PyAnextAwaitable_Type -
|
||||
|
|
Can't render this file because it has a wrong number of fields in line 4.
|
Loading…
Reference in New Issue