diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index fd221fccad5..db5f374e760 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -616,6 +616,44 @@ always available. Thus ``2.1.0a3`` is hexversion ``0x020100a3``. + +.. data:: implementation + + An object containing the information about the implementation of the + currently running Python interpreter. Its attributes are the those + that all Python implementations must implement. They are described + below. + + *name* is the implementation's identifier, like ``'cpython'``. + + *version* is a named tuple, in the same format as + :data:`sys.version_info`. It represents the version of the Python + *implementation*. This has a distinct meaning from the specific + version of the Python *language* to which the currently running + interpreter conforms, which ``sys.version_info`` represents. For + example, for PyPy 1.8 ``sys.implementation.version`` might be + ``sys.version_info(1, 8, 0, 'final', 0)``, whereas ``sys.version_info`` + would be ``sys.version_info(1, 8, 0, 'final', 0)``. For CPython they + are the same value, since it is the reference implementation. + + *hexversion* is the implementation version in hexadecimal format, like + :data:`sys.hexversion`. + + *cache_tag* is the tag used by the import machinery in the filenames of + cached modules. By convention, it would be a composite of the + implementation's name and version, like ``'cpython-33'``. However, a + Python implementation may use some other value if appropriate. If + ``cache_tag`` is set to ``None``, it indicates that module caching should + be disabled. + + Regardless of its contents, :data:`sys.implementation` will not + change during a run of the interpreter, nor between implementation + versions. (It may change between Python language versions, + however.) See `PEP 421` for more information. + + .. versionadded:: 3.3 + + .. data:: int_info A :term:`struct sequence` that holds information about Python's internal diff --git a/Doc/library/types.rst b/Doc/library/types.rst index fc26afabf35..b60b1958a8b 100644 --- a/Doc/library/types.rst +++ b/Doc/library/types.rst @@ -194,3 +194,27 @@ Standard names are defined for the following types: Return a new view of the underlying mapping's values. +.. class:: SimpleNamespace + + A simple :class:`object` subclass that provides attribute access to its + namespace, as well as a meaningful repr. + + Unlike :class:`object`, with ``SimpleNamespace`` you can add and remove + attributes. If a ``SimpleNamespace`` object is initialized with keyword + arguments, those are directly added to the underlying namespace. + + The type is roughly equivalent to the following code:: + + class SimpleNamespace: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + def __repr__(self): + keys = sorted(self.__dict__) + items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) + return "{}({})".format(type(self).__name__, ", ".join(items)) + + ``SimpleNamespace`` may be useful as a replacement for ``class NS: pass``. + However, for a structured record type use :func:`~collections.namedtuple` + instead. + + .. versionadded:: 3.3 diff --git a/Include/Python.h b/Include/Python.h index 6affbf5ede1..a78a7219269 100644 --- a/Include/Python.h +++ b/Include/Python.h @@ -101,6 +101,7 @@ #include "warnings.h" #include "weakrefobject.h" #include "structseq.h" +#include "namespaceobject.h" #include "codecs.h" #include "pyerrors.h" diff --git a/Include/namespaceobject.h b/Include/namespaceobject.h new file mode 100644 index 00000000000..a412f05200c --- /dev/null +++ b/Include/namespaceobject.h @@ -0,0 +1,17 @@ + +/* simple namespace object interface */ + +#ifndef NAMESPACEOBJECT_H +#define NAMESPACEOBJECT_H +#ifdef __cplusplus +extern "C" { +#endif + +PyAPI_DATA(PyTypeObject) _PyNamespace_Type; + +PyAPI_FUNC(PyObject *) _PyNamespace_New(PyObject *kwds); + +#ifdef __cplusplus +} +#endif +#endif /* !NAMESPACEOBJECT_H */ diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index b024d9a261c..a9c3616645b 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -581,6 +581,24 @@ class SysModuleTest(unittest.TestCase): expected = None self.check_fsencoding(fs_encoding, expected) + def test_implementation(self): + # This test applies to all implementations equally. + + levels = {'alpha': 0xA, 'beta': 0xB, 'candidate': 0xC, 'release': 0xF} + + self.assertTrue(hasattr(sys.implementation, 'name')) + self.assertTrue(hasattr(sys.implementation, 'version')) + self.assertTrue(hasattr(sys.implementation, 'hexversion')) + self.assertTrue(hasattr(sys.implementation, 'cache_tag')) + + version = sys.implementation.version + self.assertEqual(version[:2], (version.major, version.minor)) + + hexversion = (version.major << 24 | version.minor << 16 | + version.micro << 8 | levels[version.releaselevel] << 4 | + version.serial << 0) + self.assertEqual(sys.implementation.hexversion, hexversion) + class SizeofTest(unittest.TestCase): diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 51b594cf5eb..31ebd9c603b 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -996,8 +996,149 @@ class ClassCreationTests(unittest.TestCase): X = types.new_class("X", (int(), C)) +class SimpleNamespaceTests(unittest.TestCase): + + def test_constructor(self): + ns1 = types.SimpleNamespace() + ns2 = types.SimpleNamespace(x=1, y=2) + ns3 = types.SimpleNamespace(**dict(x=1, y=2)) + + with self.assertRaises(TypeError): + types.SimpleNamespace(1, 2, 3) + + self.assertEqual(len(ns1.__dict__), 0) + self.assertEqual(vars(ns1), {}) + self.assertEqual(len(ns2.__dict__), 2) + self.assertEqual(vars(ns2), {'y': 2, 'x': 1}) + self.assertEqual(len(ns3.__dict__), 2) + self.assertEqual(vars(ns3), {'y': 2, 'x': 1}) + + def test_unbound(self): + ns1 = vars(types.SimpleNamespace()) + ns2 = vars(types.SimpleNamespace(x=1, y=2)) + + self.assertEqual(ns1, {}) + self.assertEqual(ns2, {'y': 2, 'x': 1}) + + def test_underlying_dict(self): + ns1 = types.SimpleNamespace() + ns2 = types.SimpleNamespace(x=1, y=2) + ns3 = types.SimpleNamespace(a=True, b=False) + mapping = ns3.__dict__ + del ns3 + + self.assertEqual(ns1.__dict__, {}) + self.assertEqual(ns2.__dict__, {'y': 2, 'x': 1}) + self.assertEqual(mapping, dict(a=True, b=False)) + + def test_attrget(self): + ns = types.SimpleNamespace(x=1, y=2, w=3) + + self.assertEqual(ns.x, 1) + self.assertEqual(ns.y, 2) + self.assertEqual(ns.w, 3) + with self.assertRaises(AttributeError): + ns.z + + def test_attrset(self): + ns1 = types.SimpleNamespace() + ns2 = types.SimpleNamespace(x=1, y=2, w=3) + ns1.a = 'spam' + ns1.b = 'ham' + ns2.z = 4 + ns2.theta = None + + self.assertEqual(ns1.__dict__, dict(a='spam', b='ham')) + self.assertEqual(ns2.__dict__, dict(x=1, y=2, w=3, z=4, theta=None)) + + def test_attrdel(self): + ns1 = types.SimpleNamespace() + ns2 = types.SimpleNamespace(x=1, y=2, w=3) + + with self.assertRaises(AttributeError): + del ns1.spam + with self.assertRaises(AttributeError): + del ns2.spam + + del ns2.y + self.assertEqual(vars(ns2), dict(w=3, x=1)) + ns2.y = 'spam' + self.assertEqual(vars(ns2), dict(w=3, x=1, y='spam')) + del ns2.y + self.assertEqual(vars(ns2), dict(w=3, x=1)) + + ns1.spam = 5 + self.assertEqual(vars(ns1), dict(spam=5)) + del ns1.spam + self.assertEqual(vars(ns1), {}) + + def test_repr(self): + ns1 = types.SimpleNamespace(x=1, y=2, w=3) + ns2 = types.SimpleNamespace() + ns2.x = "spam" + ns2._y = 5 + + self.assertEqual(repr(ns1), "namespace(w=3, x=1, y=2)") + self.assertEqual(repr(ns2), "namespace(_y=5, x='spam')") + + def test_nested(self): + ns1 = types.SimpleNamespace(a=1, b=2) + ns2 = types.SimpleNamespace() + ns3 = types.SimpleNamespace(x=ns1) + ns2.spam = ns1 + ns2.ham = '?' + ns2.spam = ns3 + + self.assertEqual(vars(ns1), dict(a=1, b=2)) + self.assertEqual(vars(ns2), dict(spam=ns3, ham='?')) + self.assertEqual(ns2.spam, ns3) + self.assertEqual(vars(ns3), dict(x=ns1)) + self.assertEqual(ns3.x.a, 1) + + def test_recursive(self): + ns1 = types.SimpleNamespace(c='cookie') + ns2 = types.SimpleNamespace() + ns3 = types.SimpleNamespace(x=1) + ns1.spam = ns1 + ns2.spam = ns3 + ns3.spam = ns2 + + self.assertEqual(ns1.spam, ns1) + self.assertEqual(ns1.spam.spam, ns1) + self.assertEqual(ns1.spam.spam, ns1.spam) + self.assertEqual(ns2.spam, ns3) + self.assertEqual(ns3.spam, ns2) + self.assertEqual(ns2.spam.spam, ns2) + + def test_recursive_repr(self): + ns1 = types.SimpleNamespace(c='cookie') + ns2 = types.SimpleNamespace() + ns3 = types.SimpleNamespace(x=1) + ns1.spam = ns1 + ns2.spam = ns3 + ns3.spam = ns2 + + self.assertEqual(repr(ns1), + "namespace(c='cookie', spam=namespace(...))") + self.assertEqual(repr(ns2), + "namespace(spam=namespace(spam=namespace(...), x=1))") + + def test_as_dict(self): + ns = types.SimpleNamespace(spam='spamspamspam') + + with self.assertRaises(TypeError): + len(ns) + with self.assertRaises(TypeError): + iter(ns) + with self.assertRaises(TypeError): + 'spam' in ns + with self.assertRaises(TypeError): + ns['spam'] + + def test_main(): - run_unittest(TypesTests, MappingProxyTests, ClassCreationTests) + run_unittest(TypesTests, MappingProxyTests, ClassCreationTests, + SimpleNamespaceTests) if __name__ == '__main__': test_main() diff --git a/Lib/types.py b/Lib/types.py index 2bfcd9be8d9..cfd09eaaffe 100644 --- a/Lib/types.py +++ b/Lib/types.py @@ -13,6 +13,7 @@ FunctionType = type(_f) LambdaType = type(lambda: None) # Same as FunctionType CodeType = type(_f.__code__) MappingProxyType = type(type.__dict__) +SimpleNamespace = type(sys.implementation) def _g(): yield 1 diff --git a/Makefile.pre.in b/Makefile.pre.in index cac242f2041..5c56c038c21 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -392,6 +392,7 @@ OBJECT_OBJS= \ Objects/memoryobject.o \ Objects/methodobject.o \ Objects/moduleobject.o \ + Objects/namespaceobject.o \ Objects/object.o \ Objects/obmalloc.o \ Objects/capsule.o \ @@ -766,6 +767,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/methodobject.h \ $(srcdir)/Include/modsupport.h \ $(srcdir)/Include/moduleobject.h \ + $(srcdir)/Include/namespaceobject.h \ $(srcdir)/Include/node.h \ $(srcdir)/Include/object.h \ $(srcdir)/Include/objimpl.h \ diff --git a/Objects/namespaceobject.c b/Objects/namespaceobject.c new file mode 100644 index 00000000000..753874c3cbe --- /dev/null +++ b/Objects/namespaceobject.c @@ -0,0 +1,225 @@ +/* namespace object implementation */ + +#include "Python.h" +#include "structmember.h" + + +typedef struct { + PyObject_HEAD + PyObject *ns_dict; +} _PyNamespaceObject; + + +static PyMemberDef namespace_members[] = { + {"__dict__", T_OBJECT, offsetof(_PyNamespaceObject, ns_dict), READONLY}, + {NULL} +}; + + +/* Methods */ + +static PyObject * +namespace_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + _PyNamespaceObject *ns; + ns = PyObject_GC_New(_PyNamespaceObject, &_PyNamespace_Type); + if (ns == NULL) + return NULL; + + ns->ns_dict = PyDict_New(); + if (ns->ns_dict == NULL) { + Py_DECREF(ns); + return NULL; + } + + PyObject_GC_Track(ns); + return (PyObject *)ns; +} + + +static int +namespace_init(_PyNamespaceObject *ns, PyObject *args, PyObject *kwds) +{ + /* ignore args if it's NULL or empty */ + if (args != NULL) { + Py_ssize_t argcount = PyObject_Size(args); + if (argcount < 0) + return argcount; + else if (argcount > 0) { + PyErr_Format(PyExc_TypeError, "no positional arguments expected"); + return -1; + } + } + if (kwds == NULL) + return 0; + return PyDict_Update(ns->ns_dict, kwds); +} + + +static void +namespace_dealloc(_PyNamespaceObject *ns) +{ + PyObject_GC_UnTrack(ns); + Py_CLEAR(ns->ns_dict); + Py_TYPE(ns)->tp_free((PyObject *)ns); +} + + +static PyObject * +namespace_repr(_PyNamespaceObject *ns) +{ + int i, loop_error = 0; + PyObject *pairs = NULL, *d = NULL, *keys = NULL, *keys_iter = NULL; + PyObject *key; + PyObject *separator, *pairsrepr, *repr = NULL; + + i = Py_ReprEnter((PyObject *)ns); + if (i != 0) { + return i > 0 ? PyUnicode_FromString("namespace(...)") : NULL; + } + + pairs = PyList_New(0); + if (pairs == NULL) + goto error; + + d = ((_PyNamespaceObject *)ns)->ns_dict; + assert(d != NULL); + Py_INCREF(d); + + keys = PyDict_Keys(d); + if (keys == NULL) + goto error; + if (PyList_Sort(keys) != 0) + goto error; + + keys_iter = PyObject_GetIter(keys); + if (keys_iter == NULL) + goto error; + + while ((key = PyIter_Next(keys_iter)) != NULL) { + if (PyUnicode_Check(key) && PyUnicode_GET_SIZE(key) > 0) { + PyObject *value, *item; + + value = PyDict_GetItem(d, key); + assert(value != NULL); + + item = PyUnicode_FromFormat("%S=%R", key, value); + if (item == NULL) { + loop_error = 1; + } + else { + loop_error = PyList_Append(pairs, item); + Py_DECREF(item); + } + } + + Py_DECREF(key); + if (loop_error) + goto error; + } + + separator = PyUnicode_FromString(", "); + if (separator == NULL) + goto error; + + pairsrepr = PyUnicode_Join(separator, pairs); + Py_DECREF(separator); + if (pairsrepr == NULL) + goto error; + + repr = PyUnicode_FromFormat("%s(%S)", + ((PyObject *)ns)->ob_type->tp_name, pairsrepr); + Py_DECREF(pairsrepr); + +error: + Py_XDECREF(pairs); + Py_XDECREF(d); + Py_XDECREF(keys); + Py_XDECREF(keys_iter); + Py_ReprLeave((PyObject *)ns); + + return repr; +} + + +static int +namespace_traverse(_PyNamespaceObject *ns, visitproc visit, void *arg) +{ + Py_VISIT(ns->ns_dict); + return 0; +} + + +static int +namespace_clear(_PyNamespaceObject *ns) +{ + Py_CLEAR(ns->ns_dict); + return 0; +} + + +PyDoc_STRVAR(namespace_doc, +"A simple attribute-based namespace.\n\ +\n\ +namespace(**kwargs)"); + +PyTypeObject _PyNamespace_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "namespace", /* tp_name */ + sizeof(_PyNamespaceObject), /* tp_size */ + 0, /* tp_itemsize */ + (destructor)namespace_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + (reprfunc)namespace_repr, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + PyObject_GenericSetAttr, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_BASETYPE, /* tp_flags */ + namespace_doc, /* tp_doc */ + (traverseproc)namespace_traverse, /* tp_traverse */ + (inquiry)namespace_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + namespace_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + offsetof(_PyNamespaceObject, ns_dict), /* tp_dictoffset */ + (initproc)namespace_init, /* tp_init */ + PyType_GenericAlloc, /* tp_alloc */ + (newfunc)namespace_new, /* tp_new */ + PyObject_GC_Del, /* tp_free */ +}; + + +PyObject * +_PyNamespace_New(PyObject *kwds) +{ + PyObject *ns = namespace_new(&_PyNamespace_Type, NULL, NULL); + if (ns == NULL) + return NULL; + + if (kwds == NULL) + return ns; + if (PyDict_Update(((_PyNamespaceObject *)ns)->ns_dict, kwds) != 0) { + Py_DECREF(ns); + return NULL; + } + + return (PyObject *)ns; +} diff --git a/Objects/object.c b/Objects/object.c index 20eaf31c74c..1211cc3154d 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1707,6 +1707,9 @@ _Py_ReadyTypes(void) if (PyType_Ready(&PyZip_Type) < 0) Py_FatalError("Can't initialize zip type"); + + if (PyType_Ready(&_PyNamespace_Type) < 0) + Py_FatalError("Can't initialize namespace type"); } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 57f880e9ec4..096e51e9f90 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -1261,6 +1261,7 @@ executable -- absolute path of the executable binary of the Python interpreter\n float_info -- a struct sequence with information about the float implementation.\n\ float_repr_style -- string indicating the style of repr() output for floats\n\ hexversion -- version information encoded as a single integer\n\ +implementation -- Python implementation information.\n\ int_info -- a struct sequence with information about the int implementation.\n\ maxsize -- the largest supported length of containers.\n\ maxunicode -- the value of the largest Unicode codepoint\n\ @@ -1454,6 +1455,69 @@ make_version_info(void) return version_info; } +static PyObject * +make_impl_info(PyObject *version_info) +{ + int res; + PyObject *impl_info, *value, *ns; + + impl_info = PyDict_New(); + if (impl_info == NULL) + return NULL; + + /* populate the dict */ + +#define NAME "cpython" +#define QUOTE(arg) #arg +#define STRIFY(name) QUOTE(name) +#define MAJOR STRIFY(PY_MAJOR_VERSION) +#define MINOR STRIFY(PY_MINOR_VERSION) +#define TAG NAME "-" MAJOR MINOR + value = PyUnicode_FromString(NAME); + if (value == NULL) + goto error; + res = PyDict_SetItemString(impl_info, "name", value); + Py_DECREF(value); + if (res < 0) + goto error; + + value = PyUnicode_FromString(TAG); + if (value == NULL) + goto error; + res = PyDict_SetItemString(impl_info, "cache_tag", value); + Py_DECREF(value); + if (res < 0) + goto error; +#undef NAME +#undef QUOTE +#undef STRIFY +#undef MAJOR +#undef MINOR +#undef TAG + + res = PyDict_SetItemString(impl_info, "version", version_info); + if (res < 0) + goto error; + + value = PyLong_FromLong(PY_VERSION_HEX); + if (value == NULL) + goto error; + res = PyDict_SetItemString(impl_info, "hexversion", value); + Py_DECREF(value); + if (res < 0) + goto error; + + /* dict ready */ + + ns = _PyNamespace_New(impl_info); + Py_DECREF(impl_info); + return ns; + +error: + Py_CLEAR(impl_info); + return NULL; +} + static struct PyModuleDef sysmodule = { PyModuleDef_HEAD_INIT, "sys", @@ -1469,7 +1533,7 @@ static struct PyModuleDef sysmodule = { PyObject * _PySys_Init(void) { - PyObject *m, *v, *sysdict; + PyObject *m, *v, *sysdict, *version_info; char *s; m = PyModule_Create(&sysmodule); @@ -1589,11 +1653,15 @@ _PySys_Init(void) /* version_info */ if (VersionInfoType.tp_name == 0) PyStructSequence_InitType(&VersionInfoType, &version_info_desc); - SET_SYS_FROM_STRING("version_info", make_version_info()); + version_info = make_version_info(); + SET_SYS_FROM_STRING("version_info", version_info); /* prevent user from creating new instances */ VersionInfoType.tp_init = NULL; VersionInfoType.tp_new = NULL; + /* implementation */ + SET_SYS_FROM_STRING("implementation", make_impl_info(version_info)); + /* flags */ if (FlagsType.tp_name == 0) PyStructSequence_InitType(&FlagsType, &flags_desc);