44
55from datetime import datetime
66
7+ import pytest
8+
79import attr
810
911
@@ -30,7 +32,7 @@ class C:
3032 y = attr .ib (type = int )
3133 z : float = attr .ib ()
3234
33- assert results == [("x" , None ), ("y" , int ), ("z" , float )]
35+ assert [("x" , None ), ("y" , int ), ("z" , float )] == results
3436
3537 def test_hook_applied_auto_attrib (self ):
3638 """
@@ -49,7 +51,7 @@ class C:
4951 x : int
5052 y : str = attr .ib ()
5153
52- assert results == [("x" , int ), ("y" , str )]
54+ assert [("x" , int ), ("y" , str )] == results
5355
5456 def test_hook_applied_modify_attrib (self ):
5557 """
@@ -66,7 +68,8 @@ class C:
6668 y : float
6769
6870 c = C (x = "3" , y = "3.14" )
69- assert c == C (x = 3 , y = 3.14 )
71+
72+ assert C (x = 3 , y = 3.14 ) == c
7073
7174 def test_hook_remove_field (self ):
7275 """
@@ -82,7 +85,7 @@ class C:
8285 x : int
8386 y : float
8487
85- assert attr .asdict (C (2.7 )) == { "y" : 2.7 }
88+ assert { "y" : 2.7 } == attr .asdict (C (2.7 ))
8689
8790 def test_hook_add_field (self ):
8891 """
@@ -98,7 +101,7 @@ def hook(cls, attribs):
98101 class C :
99102 x : int
100103
101- assert attr . asdict ( C ( 1 , 2 )) == {"x" : 1 , "new" : 2 }
104+ assert {"x" : 1 , "new" : 2 } == attr . asdict ( C ( 1 , 2 ))
102105
103106 def test_hook_override_alias (self ):
104107 """
@@ -118,13 +121,73 @@ class NameCase:
118121 1 , 2 , 3
119122 )
120123
124+ def test_hook_reorder_fields (self ):
125+ """
126+ It is possible to reorder fields via the hook.
127+ """
128+
129+ def hook (cls , attribs ):
130+ return sorted (attribs , key = lambda x : x .metadata ["field_order" ])
131+
132+ @attr .s (field_transformer = hook )
133+ class C :
134+ x : int = attr .ib (metadata = {"field_order" : 1 })
135+ y : int = attr .ib (metadata = {"field_order" : 0 })
136+
137+ assert {"x" : 0 , "y" : 1 } == attr .asdict (C (1 , 0 ))
138+
139+ def test_hook_reorder_fields_before_order_check (self ):
140+ """
141+ It is possible to reorder fields via the hook before order-based errors are raised.
142+
143+ Regression test for #1147.
144+ """
145+
146+ def hook (cls , attribs ):
147+ return sorted (attribs , key = lambda x : x .metadata ["field_order" ])
148+
149+ @attr .s (field_transformer = hook )
150+ class C :
151+ x : int = attr .ib (metadata = {"field_order" : 1 }, default = 0 )
152+ y : int = attr .ib (metadata = {"field_order" : 0 })
153+
154+ assert {"x" : 0 , "y" : 1 } == attr .asdict (C (1 ))
155+
156+ def test_hook_conflicting_defaults_after_reorder (self ):
157+ """
158+ Raises `ValueError` if attributes with defaults are followed by
159+ mandatory attributes after the hook reorders fields.
160+
161+ Regression test for #1147.
162+ """
163+
164+ def hook (cls , attribs ):
165+ return sorted (attribs , key = lambda x : x .metadata ["field_order" ])
166+
167+ with pytest .raises (ValueError ) as e :
168+
169+ @attr .s (field_transformer = hook )
170+ class C :
171+ x : int = attr .ib (metadata = {"field_order" : 1 })
172+ y : int = attr .ib (metadata = {"field_order" : 0 }, default = 0 )
173+
174+ assert (
175+ "No mandatory attributes allowed after an attribute with a "
176+ "default value or factory. Attribute in question: Attribute"
177+ "(name='x', default=NOTHING, validator=None, repr=True, "
178+ "eq=True, eq_key=None, order=True, order_key=None, "
179+ "hash=None, init=True, "
180+ "metadata=mappingproxy({'field_order': 1}), type='int', converter=None, "
181+ "kw_only=False, inherited=False, on_setattr=None, alias=None)" ,
182+ ) == e .value .args
183+
121184 def test_hook_with_inheritance (self ):
122185 """
123186 The hook receives all fields from base classes.
124187 """
125188
126189 def hook (cls , attribs ):
127- assert [a .name for a in attribs ] == [ "x" , "y" ]
190+ assert ["x" , "y" ] == [ a .name for a in attribs ]
128191 # Remove Base' "x"
129192 return attribs [1 :]
130193
@@ -136,7 +199,7 @@ class Base:
136199 class Sub (Base ):
137200 y : int
138201
139- assert attr .asdict (Sub (2 )) == { "y" : 2 }
202+ assert { "y" : 2 } == attr .asdict (Sub (2 ))
140203
141204 def test_attrs_attrclass (self ):
142205 """
@@ -151,7 +214,7 @@ class C:
151214 x : int
152215
153216 fields_type = type (attr .fields (C ))
154- assert fields_type . __name__ == "CAttributes"
217+ assert "CAttributes" == fields_type . __name__
155218 assert issubclass (fields_type , tuple )
156219
157220
@@ -187,12 +250,12 @@ class Parent:
187250 )
188251
189252 result = attr .asdict (inst , value_serializer = hook )
190- assert result == {
253+ assert {
191254 "a" : {"x" : 1 , "y" : ["2020-07-01T00:00:00" ]},
192255 "b" : [{"x" : 2 , "y" : ["2020-07-02T00:00:00" ]}],
193256 "c" : {"spam" : {"x" : 3 , "y" : ["2020-07-03T00:00:00" ]}},
194257 "d" : {"eggs" : "2020-07-04T00:00:00" },
195- }
258+ } == result
196259
197260 def test_asdict_calls (self ):
198261 """
@@ -217,12 +280,12 @@ class Parent:
217280 inst = Parent (a = Child (1 ), b = [Child (2 )], c = {"spam" : Child (3 )})
218281
219282 attr .asdict (inst , value_serializer = hook )
220- assert calls == [
283+ assert [
221284 (inst , "a" , inst .a ),
222285 (inst .a , "x" , inst .a .x ),
223286 (inst , "b" , inst .b ),
224287 (inst .b [0 ], "x" , inst .b [0 ].x ),
225288 (inst , "c" , inst .c ),
226289 (None , None , "spam" ),
227290 (inst .c ["spam" ], "x" , inst .c ["spam" ].x ),
228- ]
291+ ] == calls
0 commit comments