Skip to content

Commit 4361555

Browse files
author
Sylvain MARIE
committed
Default string representation in @autodict is now more readable. Legacy representation is still available through a parameter. Fixed #29
Fixed `@autodict` behaviour when the list was `vars(self)` and used together with `@autoprops`: with some options the private names were appearing and with others the public property names were appearing. Now the public property names always appear if they exist.
1 parent f1a32f3 commit 4361555

2 files changed

Lines changed: 40 additions & 20 deletions

File tree

autoclass/autodict_.py

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from autoclass.autoprops_ import DuplicateOverrideError
2323
from autoclass.utils import is_attr_selected, method_already_there, possibly_replace_with_property_name, \
24-
check_known_decorators, AUTO, read_fields
24+
check_known_decorators, AUTO, read_fields, iterate_on_vars
2525

2626
from decopatch import class_decorator, DECORATED
2727

@@ -34,6 +34,7 @@ def autodict(include=None, # type: Union[str, Tuple[str]]
3434
exclude=None, # type: Union[str, Tuple[str]]
3535
only_known_fields=True, # type: bool
3636
only_public_fields=True, # type: bool
37+
legacy_str_repr=False, # type: bool
3738
only_constructor_args=AUTO, # type: bool
3839
cls=DECORATED
3940
):
@@ -57,17 +58,21 @@ def autodict(include=None, # type: Union[str, Tuple[str]]
5758
:param only_public_fields: this parameter is only used when only_constructor_args is set to False. If
5859
only_public_fields is set to False, all fields are visible. Otherwise (default), class-private fields will be
5960
hidden
61+
:param legacy_str_repr: turn this to `True` to get the legacy string representation `{%r: %r, ...}` instead of
62+
the new default one `(%s=%r, ...)`
6063
:return:
6164
"""
6265
return autodict_decorate(cls, include=include, exclude=exclude, only_constructor_args=only_constructor_args,
63-
only_public_fields=only_public_fields, only_known_fields=only_known_fields)
66+
only_public_fields=only_public_fields, only_known_fields=only_known_fields,
67+
legacy_str_repr=legacy_str_repr)
6468

6569

6670
def autodict_decorate(cls, # type: Type[T]
6771
include=None, # type: Union[str, Tuple[str]]
6872
exclude=None, # type: Union[str, Tuple[str]]
6973
only_known_fields=True, # type: bool
7074
only_public_fields=True, # type: bool
75+
legacy_str_repr=False, # type: bool
7176
only_constructor_args=AUTO, # type: bool
7277
):
7378
# type: (...) -> Type[T]
@@ -85,6 +90,8 @@ def autodict_decorate(cls, # type: Type[T]
8590
:param only_public_fields: this parameter is only used when only_constructor_args is set to False. If
8691
only_public_fields is set to False, all fields are visible. Otherwise (default), class-private fields will be
8792
hidden
93+
:param legacy_str_repr: turn this to `True` to get the legacy string representation `{%r: %r, ...}` instead of
94+
the new default one `(%s=%r, ...)`
8895
:return:
8996
"""
9097
if only_constructor_args is not AUTO:
@@ -104,10 +111,11 @@ def autodict_decorate(cls, # type: Type[T]
104111
selected_names, source = read_fields(cls, include=include, exclude=exclude, caller="@autodict")
105112

106113
# add autohash with explicit list
107-
execute_autodict_on_class(cls, selected_names=selected_names)
114+
execute_autodict_on_class(cls, selected_names=selected_names, legacy_str_repr=legacy_str_repr)
108115
else:
109116
# no explicit list
110-
execute_autodict_on_class(cls, include=include, exclude=exclude, public_fields_only=only_public_fields)
117+
execute_autodict_on_class(cls, include=include, exclude=exclude, public_fields_only=only_public_fields,
118+
legacy_str_repr=legacy_str_repr)
111119

112120
return cls
113121

@@ -117,6 +125,7 @@ def execute_autodict_on_class(cls, # type: Type[T]
117125
include=None, # type: Union[str, Tuple[str]]
118126
exclude=None, # type: Union[str, Tuple[str]]
119127
public_fields_only=True, # type: bool
128+
legacy_str_repr=False, # type: bool
120129
):
121130
"""
122131
This method makes objects of the class behave like a read-only `dict`. It does several things:
@@ -140,6 +149,8 @@ def execute_autodict_on_class(cls, # type: Type[T]
140149
:param public_fields_only: this parameter is only used when `selected_names` is not provided. If
141150
public_fields_only is set to False, all fields are visible. Otherwise (default), class-private fields will be
142151
hidden from the exposed dict view.
152+
:param legacy_str_repr: turn this to `True` to get the legacy string representation `{%r: %r, ...}` instead of
153+
the new default one `(%s=%r, ...)`
143154
:return:
144155
"""
145156
# check if the class is already a dict-like
@@ -305,6 +316,7 @@ def __eq__(self, other):
305316
cls.__eq__ = __eq__
306317

307318
# 5. override str and repr method if not already implemented
319+
_1, _2 = "()" if legacy_str_repr else ("", "")
308320
if not method_already_there(cls, '__str__', this_class_only=True):
309321

310322
def __str__(self):
@@ -315,7 +327,7 @@ def __str__(self):
315327
:return:
316328
"""
317329
# python 2 compatibility: use self.__class__ not type()
318-
return self.__class__.__name__ + '(' + print_ordered_dict(self) + ')'
330+
return "%s%s%s%s" % (self.__class__.__name__, _1, print_ordered_dict(self, eq_mode=not legacy_str_repr), _2)
319331

320332
cls.__str__ = __str__
321333

@@ -328,27 +340,33 @@ def __repr__(self):
328340
:return:
329341
"""
330342
# python 2 compatibility: use self.__class__ not type()
331-
return self.__class__.__name__ + '(' + print_ordered_dict(self) + ')'
343+
return "%s%s%s%s" % (self.__class__.__name__, _1, print_ordered_dict(self, eq_mode=not legacy_str_repr), _2)
332344

333345
cls.__repr__ = __repr__
334346

335347
return
336348

337349

338-
def print_ordered_dict(odict # type: Mapping
350+
def print_ordered_dict(odict, # type: Mapping
351+
eq_mode=False # type: bool
339352
):
340353
# type: (...) -> str
341354
"""
342355
Utility method to get a string representation for an ordered mapping.
343356
344357
:param odict: an ordered mapping
358+
:param eq_mode: if `False` (default) the representation will be {%r: %r} whereas otherwise it will be
359+
`(%s=%r)`
345360
:return:
346361
"""
347362
# This destroys the order
348363
# return str(dict(obj))
349364

350365
# This follows the order from __iter__
351-
return '{%s}' % ', '.join('%r: %r' % (k, v) for k, v in odict.items())
366+
if eq_mode:
367+
return '(%s)' % ', '.join('%s=%r' % (k, v) for k, v in odict.items())
368+
else:
369+
return '{%s}' % ', '.join('%r: %r' % (k, v) for k, v in odict.items())
352370

353371

354372
def autodict_override_decorate(func # type: Callable
@@ -501,7 +519,7 @@ def __iter__(self):
501519
"""
502520
Generated by @autodict. Relies on vars(self) to return the iterable of dict keys.
503521
"""
504-
return iter(vars(self))
522+
return iter(iterate_on_vars(self))
505523

506524
def __getitem__(self, key):
507525
"""
@@ -530,8 +548,8 @@ def __iter__(self):
530548
Implements the __iter__ method from collections.Iterable by relying on vars(self)
531549
PLUS the super dictionary
532550
"""
533-
return chain(vars(self),
534-
(o for o in super(cls, self).__iter__() if o not in vars(self)))
551+
return chain(iterate_on_vars(self),
552+
(o for o in super(cls, self).__iter__() if o not in iterate_on_vars(self)))
535553

536554
def __getitem__(self, key):
537555
"""
@@ -572,10 +590,7 @@ def __iter__(self):
572590
"""
573591
Generated by @autodict. Relying on a filtered vars(self) for the keys iterable
574592
"""
575-
for att_name in vars(self):
576-
# replace private names with property names if needed, so that the filter can apply correctly
577-
att_name = possibly_replace_with_property_name(self.__class__, att_name)
578-
593+
for att_name in iterate_on_vars(self):
579594
# filter based on the name (include/exclude + private/public)
580595
if is_attr_selected(att_name, include=include, exclude=exclude) and \
581596
(not public_fields_only or not att_name.startswith(private_name_prefix)):
@@ -629,11 +644,8 @@ def __iter__(self):
629644
:param self:
630645
:return:
631646
"""
632-
myattrs = (possibly_replace_with_property_name(self.__class__, att_name) for att_name in vars(self))
633-
for att_name in chain(myattrs, (o for o in super(cls, self).__iter__() if o not in vars(self))):
634-
# replace private names with property names if needed, so that the filter can apply correctly
635-
att_name = possibly_replace_with_property_name(self.__class__, att_name)
636-
647+
myattrs = tuple(att_name for att_name in iterate_on_vars(self))
648+
for att_name in chain(myattrs, (o for o in super(cls, self).__iter__() if o not in myattrs)):
637649
# filter based on the name (include/exclude + private/public)
638650
if is_attr_selected(att_name, include=include, exclude=exclude) and \
639651
(not public_fields_only or not att_name.startswith(private_name_prefix)):

autoclass/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ def method_already_there(cls,
237237
return method is not None and method is not getattr(object, method_name, None)
238238

239239

240+
def iterate_on_vars(self):
241+
""" yields all vars names, replacing them with their public property name if it exists """
242+
for att_name in vars(self):
243+
yield possibly_replace_with_property_name(self.__class__, att_name)
244+
245+
240246
def possibly_replace_with_property_name(cls,
241247
att_name # type: str
242248
):
@@ -257,6 +263,8 @@ def is_property_related_attr(cls,
257263
# type: (...) -> bool
258264
"""
259265
Returns True if the attribute name without a leading underscore corresponds to a property name in that class
266+
TODO we should extend this to all descriptors
267+
260268
:param cls:
261269
:param att_name:
262270
:return:

0 commit comments

Comments
 (0)