Skip to content

Commit 0902040

Browse files
author
Sylvain MARIE
committed
New @autorepr decorator. Previously this feature was only available through @autodict, it can now be used without it. Fixed #30
New central `__AUTOCLASS_OVERRIDE_ANNOTATION` used by several decorators
1 parent 4361555 commit 0902040

5 files changed

Lines changed: 313 additions & 12 deletions

File tree

autoclass/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from autoclass.autoargs_ import autoargs, autoargs_decorate
2-
from autoclass.autoclass_ import autoclass, autoclass_decorate
2+
from autoclass.autoprops_ import IllegalGetterSignatureException, IllegalSetterSignatureException, autoprops, \
3+
autoprops_decorate, DuplicateOverrideError, getter_override, setter_override, autoprops_override_decorate
34
from autoclass.autoslots_ import autoslots, autoslots_decorate
45
from autoclass.autodict_ import autodict, autodict_decorate, autodict_override, autodict_override_decorate, \
56
print_ordered_dict
67
from autoclass.autohash_ import autohash, autohash_decorate
7-
from autoclass.autoprops_ import IllegalGetterSignatureException, IllegalSetterSignatureException, autoprops, \
8-
autoprops_decorate, DuplicateOverrideError, getter_override, setter_override, autoprops_override_decorate
9-
from autoclass.utils import AutoclassDecorationException
8+
from autoclass.autorepr_ import autorepr, autorepr_decorate
9+
from autoclass.autoclass_ import autoclass, autoclass_decorate
10+
from autoclass.utils import AutoclassDecorationException, autoclass_override
1011

1112
try:
1213
# Distribution mode : import from _version.py generated by setuptools_scm during release
@@ -23,10 +24,11 @@
2324
'autoargs_', 'autoclass_', 'autodict_', 'autohash_', 'autoprops_', 'utils',
2425
# symbols
2526
'autoargs', 'autoargs_decorate',
26-
'autoclass', 'autoclass_decorate',
27+
'autoclass', 'autoclass_decorate', 'autoclass_override',
2728
'autoslots', 'autoslots_decorate',
2829
'autodict', 'autodict_decorate', 'autodict_override', 'autodict_override_decorate', 'print_ordered_dict',
2930
'autohash', 'autohash_decorate',
31+
'autorepr', 'autorepr_decorate',
3032
'IllegalGetterSignatureException', 'IllegalSetterSignatureException', 'autoprops', 'autoprops_decorate',
3133
'DuplicateOverrideError', 'getter_override', 'setter_override', 'autoprops_override_decorate',
3234
'AutoclassDecorationException'

autoclass/autodict_.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
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, iterate_on_vars
24+
check_known_decorators, AUTO, read_fields, __AUTOCLASS_OVERRIDE_ANNOTATION, iterate_on_vars
2525

2626
from decopatch import class_decorator, DECORATED
2727

2828

29-
__AUTODICT_OVERRIDE_ANNOTATION = '__autodict_override__'
29+
__AUTODICT_OVERRIDE_ANNOTATION = __AUTOCLASS_OVERRIDE_ANNOTATION
3030

3131

3232
@class_decorator

autoclass/autoprops_.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
from decopatch import DECORATED, function_decorator, class_decorator
2323

24-
from autoclass.utils import check_known_decorators, AUTO, read_fields_from_init
24+
from autoclass.utils import check_known_decorators, AUTO, read_fields_from_init, DuplicateOverrideError
2525

2626
__GETTER_OVERRIDE_ANNOTATION = '__getter_override__'
2727
__SETTER_OVERRIDE_ANNOTATION = '__setter_override__'
@@ -35,10 +35,6 @@ class IllegalSetterSignatureException(Exception):
3535
""" This is raised whenever an overridden setter has an illegal signature"""
3636

3737

38-
class DuplicateOverrideError(Exception):
39-
""" This is raised whenever a getter or setter is overridden twice for the same attribute"""
40-
41-
4238
@class_decorator
4339
def autoprops(include=None, # type: Union[str, Tuple[str]]
4440
exclude=None, # type: Union[str, Tuple[str]]

autoclass/autorepr_.py

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
# Authors: Sylvain Marie <sylvain.marie@se.com>
2+
#
3+
# Copyright (c) Schneider Electric Industries, 2019. All right reserved.
4+
5+
from warnings import warn
6+
7+
try: # python 3+
8+
from inspect import signature
9+
except ImportError:
10+
from funcsigs import signature
11+
12+
try:
13+
from typing import Any, Tuple, Union, Dict, TypeVar, Callable, Iterable, Sized
14+
try:
15+
from typing import Type
16+
except ImportError:
17+
pass
18+
T = TypeVar('T')
19+
except ImportError:
20+
pass
21+
22+
from autoclass.autoprops_ import DuplicateOverrideError
23+
from autoclass.utils import is_attr_selected, method_already_there, possibly_replace_with_property_name, \
24+
check_known_decorators, AUTO, read_fields, __AUTOCLASS_OVERRIDE_ANNOTATION, iterate_on_vars
25+
26+
from decopatch import class_decorator, DECORATED
27+
28+
29+
@class_decorator
30+
def autorepr(include=None, # type: Union[str, Tuple[str]]
31+
exclude=None, # type: Union[str, Tuple[str]]
32+
only_known_fields=True, # type: bool
33+
only_public_fields=True, # type: bool
34+
curly_string_repr=False, # type: bool
35+
cls=DECORATED
36+
):
37+
"""
38+
A decorator to generate str and repr method for class cls if not already implemented
39+
Parameters allow to customize the list of fields that will be visible in the representation.
40+
41+
:param include: a tuple of explicit attribute names to include (None means all)
42+
:param exclude: a tuple of explicit attribute names to exclude. In such case, include should be None.
43+
:param only_known_fields: if True (default), only known fields (constructor arguments or pyfields fields) will be
44+
exposed through the str/repr view, not any other field that would be created in the constructor or
45+
dynamically. If set to False, the dictionary is a direct view of *all* public object fields. This view can be
46+
filtered with include/exclude and private fields can be made visible by setting only_public_fields to false
47+
:param only_public_fields: this parameter is only used when only_constructor_args is set to False. If
48+
only_public_fields is set to False, all fields are visible. Otherwise (default), class-private fields will be
49+
hidden
50+
:param curly_string_repr: turn this to `True` to get the curly string representation `{%r: %r, ...}` instead of
51+
the default one `(%s=%r, ...)`
52+
:return:
53+
"""
54+
return autorepr_decorate(cls, include=include, exclude=exclude, curly_string_repr=curly_string_repr,
55+
only_public_fields=only_public_fields, only_known_fields=only_known_fields)
56+
57+
58+
def autorepr_decorate(cls, # type: Type[T]
59+
include=None, # type: Union[str, Tuple[str]]
60+
exclude=None, # type: Union[str, Tuple[str]]
61+
only_known_fields=True, # type: bool
62+
only_public_fields=True, # type: bool
63+
curly_string_repr=False, # type: bool
64+
):
65+
# type: (...) -> Type[T]
66+
"""
67+
To automatically generate the appropriate str and repr methods, without using @autodict decorator.
68+
69+
:param cls: the class on which to execute. Note that it won't be wrapped.
70+
:param include: a tuple of explicit attribute names to include (None means all)
71+
:param exclude: a tuple of explicit attribute names to exclude. In such case, include should be None.
72+
:param only_known_fields: if True (default), only known fields (constructor arguments or pyfields fields) will be
73+
exposed through the str/repr view, not any other field that would be created in the constructor or
74+
dynamically. If set to False, the dictionary is a direct view of *all* public object fields. This view can be
75+
filtered with include/exclude and private fields can be made visible by setting only_public_fields to false
76+
:param only_public_fields: this parameter is only used when only_constructor_args is set to False. If
77+
only_public_fields is set to False, all fields are visible. Otherwise (default), class-private fields will be
78+
hidden
79+
:param curly_string_repr: turn this to `True` to get the curly string representation `{%r: %r, ...}` instead of
80+
the default one `(%s=%r, ...)`
81+
:return:
82+
"""
83+
# first check that we do not conflict with other known decorators
84+
check_known_decorators(cls, '@autorepr')
85+
86+
# perform the class mod
87+
if only_known_fields:
88+
# retrieve the list of fields from pyfields or constructor signature
89+
selected_names, source = read_fields(cls, include=include, exclude=exclude, caller="@autorepr")
90+
91+
# add autohash with explicit list
92+
execute_autorepr_on_class(cls, selected_names=selected_names, curly_string_repr=curly_string_repr)
93+
else:
94+
# no explicit list
95+
execute_autorepr_on_class(cls, include=include, exclude=exclude, public_fields_only=only_public_fields,
96+
curly_string_repr=curly_string_repr)
97+
98+
return cls
99+
100+
101+
def execute_autorepr_on_class(cls, # type: Type[T]
102+
selected_names=None, # type: Iterable[str]
103+
include=None, # type: Union[str, Tuple[str]]
104+
exclude=None, # type: Union[str, Tuple[str]]
105+
public_fields_only=True, # type: bool
106+
curly_string_repr=False, # type: bool
107+
):
108+
"""
109+
This method overrides str and repr method if not already implemented
110+
111+
Parameters allow to customize the list of fields that will be visible.
112+
113+
:param cls: the class on which to execute.
114+
:param selected_names: an explicit list of attribute names that should be used in the dict. If this is provided,
115+
`include`, `exclude` and `public_fields_only` should be left as default as they are not used.
116+
:param include: a tuple of explicit attribute names to include (None means all). This parameter is only used when
117+
`selected_names` is not provided.
118+
:param exclude: a tuple of explicit attribute names to exclude. In such case, include should be None. This
119+
parameter is only used when `selected_names` is not provided.
120+
:param public_fields_only: this parameter is only used when `selected_names` is not provided. If
121+
public_fields_only is set to False, all fields are visible. Otherwise (default), class-private fields will be
122+
hidden from the exposed str/repr view.
123+
:param curly_string_repr: turn this to `True` to get the curly string representation `{%r: %r, ...}` instead of
124+
the default one `(%s=%r, ...)`
125+
:return:
126+
"""
127+
if selected_names is not None:
128+
# case (a) hardcoded list - easy: we know the exact list of fields to make visible
129+
if include is not None or exclude is not None or public_fields_only is not True:
130+
raise ValueError("`selected_names` can not be used together with `include`, `exclude` or "
131+
"`public_fields_only`")
132+
133+
str_repr_methods = create_repr_methods_for_hardcoded_list(selected_names, curly_mode=curly_string_repr)
134+
135+
else:
136+
# case (b) the list of fields is not predetermined, it will depend on vars(self)
137+
if include is None and exclude is None and not public_fields_only:
138+
# easy: all vars() are exposed
139+
str_repr_methods = create_repr_methods_for_object_vars(curly_mode=curly_string_repr)
140+
else:
141+
# harder: all fields are allowed, but there are filters on this dynamic list
142+
# private_name_prefix = '_' + object_type.__name__ + '_'
143+
private_name_prefix = '_' if public_fields_only else None
144+
str_repr_methods = create_repr_methods_for_object_vars_with_filters(curly_mode=curly_string_repr,
145+
include=include, exclude=exclude,
146+
private_name_prefix=private_name_prefix)
147+
148+
if method_already_there(cls, '__str__', this_class_only=True):
149+
if not hasattr(cls.__str__, __AUTOCLASS_OVERRIDE_ANNOTATION):
150+
warn('__str__ is already defined on class %s, it will be overridden with the one generated by '
151+
'@autorepr/@autoclass ! If you want to use your version, annotate it with @autoclass_override'
152+
% cls)
153+
cls.__str__ = str_repr_methods.str
154+
else:
155+
cls.__str__ = str_repr_methods.str
156+
157+
if method_already_there(cls, '__repr__', this_class_only=True):
158+
if not hasattr(cls.__repr__, __AUTOCLASS_OVERRIDE_ANNOTATION):
159+
warn('__repr__ is already defined on class %s, it will be overridden with the one generated by '
160+
'@autorepr/@autoclass ! If you want to use your version, annotate it with @autoclass_override'
161+
% cls)
162+
cls.__repr__ = str_repr_methods.repr
163+
else:
164+
cls.__repr__ = str_repr_methods.repr
165+
166+
167+
class ReprMethods(object):
168+
"""
169+
Container used in @autodict to exchange the various methods created
170+
"""
171+
__slots__ = 'str', 'repr'
172+
173+
def __init__(self, str, repr):
174+
self.str = str
175+
self.repr = repr
176+
177+
178+
def create_repr_methods_for_hardcoded_list(selected_names, # type: Union[Sized, Iterable[str]]
179+
curly_mode # type: bool
180+
):
181+
# type: (...) -> ReprMethods
182+
"""
183+
184+
:param selected_names:
185+
:param curly_mode:
186+
:return:
187+
"""
188+
if not curly_mode:
189+
def __repr__(self):
190+
"""
191+
Generated by @autorepr. Relies on the hardcoded list of field names and "getattr" (object) for the value.
192+
"""
193+
return '%s(%s)' % (self.__class__.__name__,
194+
', '.join('%s=%r' % (k, getattr(self, k)) for k in selected_names))
195+
196+
else:
197+
def __repr__(self):
198+
"""
199+
Generated by @autorepr. Relies on the hardcoded list of field names and "getattr" (object) for the value.
200+
"""
201+
return '%s(**{%s})' % (self.__class__.__name__,
202+
', '.join('%r: %r' % (k, getattr(self, k)) for k in selected_names))
203+
204+
return ReprMethods(str=__repr__, repr=__repr__)
205+
206+
207+
def create_repr_methods_for_object_vars(curly_mode # type: bool
208+
):
209+
# type: (...) -> ReprMethods
210+
"""
211+
212+
:param curly_mode:
213+
:return:
214+
"""
215+
if not curly_mode:
216+
def __repr__(self):
217+
"""
218+
Generated by @autorepr. Relies on the hardcoded list of field names and "getattr" (object) for the value.
219+
"""
220+
return '%s(%s)' % (self.__class__.__name__, ', '.join('%s=%r' % (k, v) for k, v in vars(self).items()))
221+
222+
else:
223+
def __repr__(self):
224+
"""
225+
Generated by @autorepr. Relies on the hardcoded list of field names and "getattr" (object) for the value.
226+
"""
227+
return '%s(**{%s})' % (self.__class__.__name__, ', '.join('%r: %r' % (k, v) for k, v in vars(self).items()))
228+
229+
return ReprMethods(str=__repr__, repr=__repr__)
230+
231+
232+
def create_repr_methods_for_object_vars_with_filters(curly_mode, # type: bool
233+
include, # type: Union[str, Tuple[str]]
234+
exclude, # type: Union[str, Tuple[str]]
235+
private_name_prefix=None # type: str
236+
):
237+
# type: (...) -> ReprMethods
238+
"""
239+
240+
:param curly_mode:
241+
:param include:
242+
:param exclude:
243+
:param private_name_prefix:
244+
:return:
245+
"""
246+
public_fields_only = private_name_prefix is not None
247+
248+
def _vars_iterator(self):
249+
"""
250+
Filters the vars(self) according to include/exclude/public_fields_only
251+
252+
:param self:
253+
:return:
254+
"""
255+
for att_name in iterate_on_vars(self):
256+
# filter based on the name (include/exclude + private/public)
257+
if is_attr_selected(att_name, include=include, exclude=exclude) and \
258+
(not public_fields_only or not att_name.startswith(private_name_prefix)):
259+
# use it
260+
yield att_name, getattr(self, att_name)
261+
262+
if not curly_mode:
263+
def __repr__(self):
264+
"""
265+
Generated by @autorepr. Relies on the hardcoded list of field names and "getattr" (object) for the value.
266+
"""
267+
return '%s(%s)' % (self.__class__.__name__, ', '.join('%s=%r' % (k, v) for k, v in _vars_iterator(self)))
268+
269+
else:
270+
def __repr__(self):
271+
"""
272+
Generated by @autorepr. Relies on the hardcoded list of field names and "getattr" (object) for the value.
273+
"""
274+
return '%s(**{%s})' % (self.__class__.__name__,
275+
', '.join('%r: %r' % (k, v) for k, v in _vars_iterator(self)))
276+
277+
return ReprMethods(str=__repr__, repr=__repr__)

autoclass/utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,32 @@
1111
from funcsigs import signature, Signature
1212

1313

14+
class DuplicateOverrideError(Exception):
15+
""" This is raised whenever a function is declared as overridden twice"""
16+
17+
18+
__AUTOCLASS_OVERRIDE_ANNOTATION = '__autoclass_override__'
19+
20+
21+
def autoclass_override(func # type: Callable
22+
):
23+
# type: (...) -> Callable
24+
"""
25+
Used to decorate a function as an explcitly overridden method (such as __iter__, __str__), so as to prevent
26+
@autoclass to override it.
27+
28+
:param func: the function on which to execute. Note that it won't be wrapped but simply annotated.
29+
:return:
30+
"""
31+
# Simply annotate the function
32+
if hasattr(func, __AUTOCLASS_OVERRIDE_ANNOTATION):
33+
raise DuplicateOverrideError('Function is overridden twice : %s' % func.__name__)
34+
else:
35+
setattr(func, __AUTOCLASS_OVERRIDE_ANNOTATION, True)
36+
37+
return func
38+
39+
1440
class Symbols(Enum):
1541
""" A few symbols used in function signatures of the `autoclass` library """
1642
AUTO = 0

0 commit comments

Comments
 (0)