Skip to content

Commit

Permalink
Issue python#24254: Preserve class attribute definition order.
Browse files Browse the repository at this point in the history
  • Loading branch information
ericsnowcurrently committed Sep 5, 2016
1 parent 4565986 commit 92a6c17
Show file tree
Hide file tree
Showing 18 changed files with 568 additions and 189 deletions.
367 changes: 188 additions & 179 deletions Doc/library/inspect.rst

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions Doc/library/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,20 @@ Dynamic Type Creation
in *kwds* argument with any ``'metaclass'`` entry removed. If no *kwds*
argument is passed in, this will be an empty dict.

.. impl-detail::

CPython uses :class:`collections.OrderedDict` for the default
namespace.

.. versionadded:: 3.3

.. versionchanged:: 3.6

The default value for the ``namespace`` element of the returned
tuple has changed from :func:`dict`. Now an insertion-order-
preserving mapping is used when the metaclass does not have a
``__prepare__`` method,

.. seealso::

:ref:`metaclasses`
Expand Down
11 changes: 11 additions & 0 deletions Doc/reference/compound_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,17 @@ list for the base classes and the saved local namespace for the attribute
dictionary. The class name is bound to this class object in the original local
namespace.

The order in which attributes are defined in the class body is preserved
in the ``__definition_order__`` attribute on the new class. If that order
is not known then the attribute is set to :const:`None`. The class body
may include a ``__definition_order__`` attribute. In that case it is used
directly. The value must be a tuple of identifiers or ``None``, otherwise
:exc:`TypeError` will be raised when the class statement is executed.

.. versionchanged:: 3.6

Add ``__definition_order__`` to classes.

Class creation can be customized heavily using :ref:`metaclasses <metaclasses>`.

Classes can also be decorated: just like when decorating functions, ::
Expand Down
9 changes: 8 additions & 1 deletion Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1750,7 +1750,14 @@ as ``namespace = metaclass.__prepare__(name, bases, **kwds)`` (where the
additional keyword arguments, if any, come from the class definition).

If the metaclass has no ``__prepare__`` attribute, then the class namespace
is initialised as an empty :func:`dict` instance.
is initialised as an empty ordered mapping.

.. impl-detail::

In CPython the default is :class:`collections.OrderedDict`.

.. versionchanged:: 3.6
Defaults to :class:`collections.OrderedDict` instead of :func:`dict`.

.. seealso::

Expand Down
26 changes: 26 additions & 0 deletions Doc/whatsnew/3.6.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Windows improvements:
:pep:`4XX` - Python Virtual Environments
PEP written by Carl Meyer

.. XXX PEP 520: :ref:`Preserving Class Attribute Definition Order<whatsnew-deforder>`
New Features
============
Expand Down Expand Up @@ -271,6 +272,31 @@ Example of fatal error on buffer overflow using
(Contributed by Victor Stinner in :issue:`26516` and :issue:`26564`.)


.. _whatsnew-deforder:

PEP 520: Preserving Class Attribute Definition Order
----------------------------------------------------

Attributes in a class definition body have a natural ordering: the same
order in which the names appear in the source. This order is now
preserved in the new class's ``__definition_order__`` attribute. It is
a tuple of the attribute names, in the order in which they appear in
the class definition body.

For types that don't have a definition (e.g. builtins), or the attribute
order could not be determined, ``__definition_order__`` is ``None``.

Also, the effective default class *execution* namespace (returned from
``type.__prepare__()``) is now an insertion-order-preserving mapping.
For CPython, it is now ``collections.OrderedDict``. Note that the
class namespace, ``cls.__dict__``, is unchanged.

.. seealso::

:pep:`520` - Preserving Class Attribute Definition Order
PEP written and implemented by Eric Snow.


Other Language Changes
======================

Expand Down
2 changes: 2 additions & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ typedef struct _typeobject {

destructor tp_finalize;

PyObject *tp_deforder;

#ifdef COUNT_ALLOCS
/* these must be last and never explicitly initialized */
Py_ssize_t tp_allocs;
Expand Down
4 changes: 4 additions & 0 deletions Include/odictobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ PyAPI_FUNC(PyObject *) PyODict_New(void);
PyAPI_FUNC(int) PyODict_SetItem(PyObject *od, PyObject *key, PyObject *item);
PyAPI_FUNC(int) PyODict_DelItem(PyObject *od, PyObject *key);

#ifndef Py_LIMITED_API
PyAPI_FUNC(PyObject *) _PyODict_KeysAsTuple(PyObject *od);
#endif

/* wrappers around PyDict* functions */
#define PyODict_GetItem(od, key) PyDict_GetItem((PyObject *)od, key)
#define PyODict_GetItemWithError(od, key) \
Expand Down
192 changes: 191 additions & 1 deletion Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
import types
import unittest
import warnings
from collections import OrderedDict
from operator import neg
from test.support import TESTFN, unlink, run_unittest, check_warnings
from test.support import (TESTFN, unlink, run_unittest, check_warnings,
cpython_only)
from test.support.script_helper import assert_python_ok
try:
import pty, signal
Expand Down Expand Up @@ -1778,6 +1780,194 @@ def test_type_doc(self):
A.__doc__ = doc
self.assertEqual(A.__doc__, doc)

def test_type_definition_order_nonempty(self):
class Spam:
b = 1
c = 3
a = 2
d = 4
eggs = 2
e = 5
b = 42

self.assertEqual(Spam.__definition_order__,
('__module__', '__qualname__',
'b', 'c', 'a', 'd', 'eggs', 'e'))

def test_type_definition_order_empty(self):
class Empty:
pass

self.assertEqual(Empty.__definition_order__,
('__module__', '__qualname__'))

def test_type_definition_order_on_instance(self):
class Spam:
a = 2
b = 1
c = 3
with self.assertRaises(AttributeError):
Spam().__definition_order__

def test_type_definition_order_set_to_None(self):
class Spam:
a = 2
b = 1
c = 3
Spam.__definition_order__ = None
self.assertEqual(Spam.__definition_order__, None)

def test_type_definition_order_set_to_tuple(self):
class Spam:
a = 2
b = 1
c = 3
Spam.__definition_order__ = ('x', 'y', 'z')
self.assertEqual(Spam.__definition_order__, ('x', 'y', 'z'))

def test_type_definition_order_deleted(self):
class Spam:
a = 2
b = 1
c = 3
del Spam.__definition_order__
self.assertEqual(Spam.__definition_order__, None)

def test_type_definition_order_set_to_bad_type(self):
class Spam:
a = 2
b = 1
c = 3
Spam.__definition_order__ = 42
self.assertEqual(Spam.__definition_order__, 42)

def test_type_definition_order_builtins(self):
self.assertEqual(object.__definition_order__, None)
self.assertEqual(type.__definition_order__, None)
self.assertEqual(dict.__definition_order__, None)
self.assertEqual(type(None).__definition_order__, None)

def test_type_definition_order_dunder_names_included(self):
class Dunder:
VAR = 3
def __init__(self):
pass

self.assertEqual(Dunder.__definition_order__,
('__module__', '__qualname__',
'VAR', '__init__'))

def test_type_definition_order_only_dunder_names(self):
class DunderOnly:
__xyz__ = None
def __init__(self):
pass

self.assertEqual(DunderOnly.__definition_order__,
('__module__', '__qualname__',
'__xyz__', '__init__'))

def test_type_definition_order_underscore_names(self):
class HalfDunder:
__whether_to_be = True
or_not_to_be__ = False

self.assertEqual(HalfDunder.__definition_order__,
('__module__', '__qualname__',
'_HalfDunder__whether_to_be', 'or_not_to_be__'))

def test_type_definition_order_with_slots(self):
class Slots:
__slots__ = ('x', 'y')
a = 1
b = 2

self.assertEqual(Slots.__definition_order__,
('__module__', '__qualname__',
'__slots__', 'a', 'b'))

def test_type_definition_order_overwritten_None(self):
class OverwrittenNone:
__definition_order__ = None
a = 1
b = 2
c = 3

self.assertEqual(OverwrittenNone.__definition_order__, None)

def test_type_definition_order_overwritten_tuple(self):
class OverwrittenTuple:
__definition_order__ = ('x', 'y', 'z')
a = 1
b = 2
c = 3

self.assertEqual(OverwrittenTuple.__definition_order__,
('x', 'y', 'z'))

def test_type_definition_order_overwritten_bad_item(self):
with self.assertRaises(TypeError):
class PoorlyOverwritten:
__definition_order__ = ('a', 2, 'c')
a = 1
b = 2
c = 3

def test_type_definition_order_overwritten_bad_type(self):
with self.assertRaises(TypeError):
class PoorlyOverwritten:
__definition_order__ = ['a', 2, 'c']
a = 1
b = 2
c = 3

def test_type_definition_order_metaclass(self):
class Meta(type):
SPAM = 42

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.assertEqual(Meta.__definition_order__,
('__module__', '__qualname__',
'SPAM', '__init__'))

def test_type_definition_order_OrderedDict(self):
class Meta(type):
def __prepare__(self, *args, **kwargs):
return OrderedDict()

class WithODict(metaclass=Meta):
x='y'

self.assertEqual(WithODict.__definition_order__,
('__module__', '__qualname__', 'x'))

class Meta(type):
def __prepare__(self, *args, **kwargs):
class ODictSub(OrderedDict):
pass
return ODictSub()

class WithODictSub(metaclass=Meta):
x='y'

self.assertEqual(WithODictSub.__definition_order__,
('__module__', '__qualname__', 'x'))

@cpython_only
def test_type_definition_order_cpython(self):
# some implementations will have an ordered-by-default dict.

class Meta(type):
def __prepare__(self, *args, **kwargs):
return {}

class NotOrdered(metaclass=Meta):
x='y'

self.assertEqual(NotOrdered.__definition_order__, None)

def test_bad_args(self):
with self.assertRaises(TypeError):
type()
Expand Down
11 changes: 8 additions & 3 deletions Lib/test/test_metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
meta: C ()
ns: [('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 42), ('b', 24)]
kw: []
>>> type(C) is dict
>>> type(C) is types._DefaultClassNamespaceType
True
>>> print(sorted(C.items()))
[('__module__', 'test.test_metaclass'), ('__qualname__', 'C'), ('a', 42), ('b', 24)]
Expand Down Expand Up @@ -211,8 +211,11 @@
The default metaclass must define a __prepare__() method.
>>> type.__prepare__()
{}
>>> ns = type.__prepare__()
>>> type(ns) is types._DefaultClassNamespaceType
True
>>> list(ns) == []
True
>>>
Make sure it works with subclassing.
Expand Down Expand Up @@ -248,7 +251,9 @@
"""

from collections import OrderedDict
import sys
import types

# Trace function introduces __locals__ which causes various tests to fail.
if hasattr(sys, 'gettrace') and sys.gettrace():
Expand Down
8 changes: 7 additions & 1 deletion Lib/test/test_pydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ def test_html_doc(self):
expected_html = expected_html_pattern % (
(mod_url, mod_file, doc_loc) +
expected_html_data_docstrings)
self.maxDiff = None
self.assertEqual(result, expected_html)

@unittest.skipIf(sys.flags.optimize >= 2,
Expand Down Expand Up @@ -473,13 +474,18 @@ def test_getpager_with_stdin_none(self):
def test_non_str_name(self):
# issue14638
# Treat illegal (non-str) name like no name
# Definition order is set to None so it looks the same in both
# cases.
class A:
__definition_order__ = None
__name__ = 42
class B:
pass
adoc = pydoc.render_doc(A())
bdoc = pydoc.render_doc(B())
self.assertEqual(adoc.replace("A", "B"), bdoc)
self.maxDiff = None
expected = adoc.replace("A", "B")
self.assertEqual(bdoc, expected)

def test_not_here(self):
missing_module = "test.i_am_not_here"
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ def delx(self): del self.__x
check((1,2,3), vsize('') + 3*self.P)
# type
# static type: PyTypeObject
fmt = 'P2n15Pl4Pn9Pn11PIP'
fmt = 'P2n15Pl4Pn9Pn11PIPP'
if hasattr(sys, 'getcounts'):
fmt += '3n2P'
s = vsize(fmt)
Expand Down
Loading

0 comments on commit 92a6c17

Please sign in to comment.