Aluode/PerceptionLabPortable
0
1"""This module contains the _EstimatorPrettyPrinter class used in
2BaseEstimator.__repr__ for pretty-printing estimators"""
3
4# Authors: The scikit-learn developers
5# SPDX-License-Identifier: BSD-3-Clause
6
7# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
8# 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 Python Software Foundation;
9# All Rights Reserved
10
11# Authors: Fred L. Drake, Jr. <fdrake@acm.org> (built-in CPython pprint module)
12# Nicolas Hug (scikit-learn specific changes)
13
14# License: PSF License version 2 (see below)
15
16# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
17# --------------------------------------------
18
19# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"),
20# and the Individual or Organization ("Licensee") accessing and otherwise
21# using this software ("Python") in source or binary form and its associated
22# documentation.
23
24# 2. Subject to the terms and conditions of this License Agreement, PSF hereby
25# grants Licensee a nonexclusive, royalty-free, world-wide license to
26# reproduce, analyze, test, perform and/or display publicly, prepare
27# derivative works, distribute, and otherwise use Python alone or in any
28# derivative version, provided, however, that PSF's License Agreement and
29# PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004,
30# 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016,
31# 2017, 2018 Python Software Foundation; All Rights Reserved" are retained in
32# Python alone or in any derivative version prepared by Licensee.
33
34# 3. In the event Licensee prepares a derivative work that is based on or
35# incorporates Python or any part thereof, and wants to make the derivative
36# work available to others as provided herein, then Licensee hereby agrees to
37# include in any such work a brief summary of the changes made to Python.
38
39# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES
40# NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT
41# NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF
42# MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF
43# PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.
44
45# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY
46# INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF
47# MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE
48# THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
49
50# 6. This License Agreement will automatically terminate upon a material
51# breach of its terms and conditions.
52
53# 7. Nothing in this License Agreement shall be deemed to create any
54# relationship of agency, partnership, or joint venture between PSF and
55# Licensee. This License Agreement does not grant permission to use PSF
56# trademarks or trade name in a trademark sense to endorse or promote products
57# or services of Licensee, or any third party.
58
59# 8. By copying, installing or otherwise using Python, Licensee agrees to be
60# bound by the terms and conditions of this License Agreement.
61
62
63# Brief summary of changes to original code:
64# - "compact" parameter is supported for dicts, not just lists or tuples
65# - estimators have a custom handler, they're not just treated as objects
66# - long sequences (lists, tuples, dict items) with more than N elements are
67# shortened using ellipsis (', ...') at the end.
68
69import inspect
70import pprint
71
72from .._config import get_config
73from ..base import BaseEstimator
74from ._missing import is_scalar_nan
75
76
77class KeyValTuple(tuple):
78 """Dummy class for correctly rendering key-value tuples from dicts."""
79
80 def __repr__(self):
81 # needed for _dispatch[tuple.__repr__] not to be overridden
82 return super().__repr__()
83
84
85class KeyValTupleParam(KeyValTuple):
86 """Dummy class for correctly rendering key-value tuples from parameters."""
87
88 pass
89
90
91def _changed_params(estimator):
92 """Return dict (param_name: value) of parameters that were given to
93 estimator with non-default values."""
94
95 params = estimator.get_params(deep=False)
96 init_func = getattr(estimator.__init__, "deprecated_original", estimator.__init__)
97 init_params = inspect.signature(init_func).parameters
98 init_params = {name: param.default for name, param in init_params.items()}
99
100 def has_changed(k, v):
101 if k not in init_params: # happens if k is part of a **kwargs
102 return True
103 if init_params[k] == inspect._empty: # k has no default value
104 return True
105 # try to avoid calling repr on nested estimators
106 if isinstance(v, BaseEstimator) and v.__class__ != init_params[k].__class__:
107 return True
108 # Use repr as a last resort. It may be expensive.
109 if repr(v) != repr(init_params[k]) and not (
110 is_scalar_nan(init_params[k]) and is_scalar_nan(v)
111 ):
112 return True
113 return False
114
115 return {k: v for k, v in params.items() if has_changed(k, v)}
116
117
118class _EstimatorPrettyPrinter(pprint.PrettyPrinter):
119 """Pretty Printer class for estimator objects.
120
121 This extends the pprint.PrettyPrinter class, because:
122 - we need estimators to be printed with their parameters, e.g.
123 Estimator(param1=value1, ...) which is not supported by default.
124 - the 'compact' parameter of PrettyPrinter is ignored for dicts, which
125 may lead to very long representations that we want to avoid.
126
127 Quick overview of pprint.PrettyPrinter (see also
128 https://stackoverflow.com/questions/49565047/pprint-with-hex-numbers):
129
130 - the entry point is the _format() method which calls format() (overridden
131 here)
132 - format() directly calls _safe_repr() for a first try at rendering the
133 object
134 - _safe_repr formats the whole object recursively, only calling itself,
135 not caring about line length or anything
136 - back to _format(), if the output string is too long, _format() then calls
137 the appropriate _pprint_TYPE() method (e.g. _pprint_list()) depending on
138 the type of the object. This where the line length and the compact
139 parameters are taken into account.
140 - those _pprint_TYPE() methods will internally use the format() method for
141 rendering the nested objects of an object (e.g. the elements of a list)
142
143 In the end, everything has to be implemented twice: in _safe_repr and in
144 the custom _pprint_TYPE methods. Unfortunately PrettyPrinter is really not
145 straightforward to extend (especially when we want a compact output), so
146 the code is a bit convoluted.
147
148 This class overrides:
149 - format() to support the changed_only parameter
150 - _safe_repr to support printing of estimators (for when they fit on a
151 single line)
152 - _format_dict_items so that dict are correctly 'compacted'
153 - _format_items so that ellipsis is used on long lists and tuples
154
155 When estimators cannot be printed on a single line, the builtin _format()
156 will call _pprint_estimator() because it was registered to do so (see
157 _dispatch[BaseEstimator.__repr__] = _pprint_estimator).
158
159 both _format_dict_items() and _pprint_estimator() use the
160 _format_params_or_dict_items() method that will format parameters and
161 key-value pairs respecting the compact parameter. This method needs another
162 subroutine _pprint_key_val_tuple() used when a parameter or a key-value
163 pair is too long to fit on a single line. This subroutine is called in
164 _format() and is registered as well in the _dispatch dict (just like
165 _pprint_estimator). We had to create the two classes KeyValTuple and
166 KeyValTupleParam for this.
167 """
168
169 def __init__(
170 self,
171 indent=1,
172 width=80,
173 depth=None,
174 stream=None,
175 *,
176 compact=False,
177 indent_at_name=True,
178 n_max_elements_to_show=None,
179 ):
180 super().__init__(indent, width, depth, stream, compact=compact)
181 self._indent_at_name = indent_at_name
182 if self._indent_at_name:
183 self._indent_per_level = 1 # ignore indent param
184 self._changed_only = get_config()["print_changed_only"]
185 # Max number of elements in a list, dict, tuple until we start using
186 # ellipsis. This also affects the number of arguments of an estimators
187 # (they are treated as dicts)
188 self.n_max_elements_to_show = n_max_elements_to_show
189
190 def format(self, object, context, maxlevels, level):
191 return _safe_repr(
192 object, context, maxlevels, level, changed_only=self._changed_only
193 )
194
195 def _pprint_estimator(self, object, stream, indent, allowance, context, level):
196 stream.write(object.__class__.__name__ + "(")
197 if self._indent_at_name:
198 indent += len(object.__class__.__name__)
199
200 if self._changed_only:
201 params = _changed_params(object)
202 else:
203 params = object.get_params(deep=False)
204
205 self._format_params(
206 sorted(params.items()), stream, indent, allowance + 1, context, level
207 )
208 stream.write(")")
209
210 def _format_dict_items(self, items, stream, indent, allowance, context, level):
211 return self._format_params_or_dict_items(
212 items, stream, indent, allowance, context, level, is_dict=True
213 )
214
215 def _format_params(self, items, stream, indent, allowance, context, level):
216 return self._format_params_or_dict_items(
217 items, stream, indent, allowance, context, level, is_dict=False
218 )
219
220 def _format_params_or_dict_items(
221 self, object, stream, indent, allowance, context, level, is_dict
222 ):
223 """Format dict items or parameters respecting the compact=True
224 parameter. For some reason, the builtin rendering of dict items doesn't
225 respect compact=True and will use one line per key-value if all cannot
226 fit in a single line.
227 Dict items will be rendered as <'key': value> while params will be
228 rendered as <key=value>. The implementation is mostly copy/pasting from
229 the builtin _format_items().
230 This also adds ellipsis if the number of items is greater than
231 self.n_max_elements_to_show.
232 """
233 write = stream.write
234 indent += self._indent_per_level
235 delimnl = ",\n" + " " * indent
236 delim = ""
237 width = max_width = self._width - indent + 1
238 it = iter(object)
239 try:
240 next_ent = next(it)
241 except StopIteration:
242 return
243 last = False
244 n_items = 0
245 while not last:
246 if n_items == self.n_max_elements_to_show:
247 write(", ...")
248 break
249 n_items += 1
250 ent = next_ent
251 try:
252 next_ent = next(it)
253 except StopIteration:
254 last = True
255 max_width -= allowance
256 width -= allowance
257 if self._compact:
258 k, v = ent
259 krepr = self._repr(k, context, level)
260 vrepr = self._repr(v, context, level)
261 if not is_dict:
262 krepr = krepr.strip("'")
263 middle = ": " if is_dict else "="
264 rep = krepr + middle + vrepr
265 w = len(rep) + 2
266 if width < w:
267 width = max_width
268 if delim:
269 delim = delimnl
270 if width >= w:
271 width -= w
272 write(delim)
273 delim = ", "
274 write(rep)
275 continue
276 write(delim)
277 delim = delimnl
278 class_ = KeyValTuple if is_dict else KeyValTupleParam
279 self._format(
280 class_(ent), stream, indent, allowance if last else 1, context, level
281 )
282
283 def _format_items(self, items, stream, indent, allowance, context, level):
284 """Format the items of an iterable (list, tuple...). Same as the
285 built-in _format_items, with support for ellipsis if the number of
286 elements is greater than self.n_max_elements_to_show.
287 """
288 write = stream.write
289 indent += self._indent_per_level
290 if self._indent_per_level > 1:
291 write((self._indent_per_level - 1) * " ")
292 delimnl = ",\n" + " " * indent
293 delim = ""
294 width = max_width = self._width - indent + 1
295 it = iter(items)
296 try:
297 next_ent = next(it)
298 except StopIteration:
299 return
300 last = False
301 n_items = 0
302 while not last:
303 if n_items == self.n_max_elements_to_show:
304 write(", ...")
305 break
306 n_items += 1
307 ent = next_ent
308 try:
309 next_ent = next(it)
310 except StopIteration:
311 last = True
312 max_width -= allowance
313 width -= allowance
314 if self._compact:
315 rep = self._repr(ent, context, level)
316 w = len(rep) + 2
317 if width < w:
318 width = max_width
319 if delim:
320 delim = delimnl
321 if width >= w:
322 width -= w
323 write(delim)
324 delim = ", "
325 write(rep)
326 continue
327 write(delim)
328 delim = delimnl
329 self._format(ent, stream, indent, allowance if last else 1, context, level)
330
331 def _pprint_key_val_tuple(self, object, stream, indent, allowance, context, level):
332 """Pretty printing for key-value tuples from dict or parameters."""
333 k, v = object
334 rep = self._repr(k, context, level)
335 if isinstance(object, KeyValTupleParam):
336 rep = rep.strip("'")
337 middle = "="
338 else:
339 middle = ": "
340 stream.write(rep)
341 stream.write(middle)
342 self._format(
343 v, stream, indent + len(rep) + len(middle), allowance, context, level
344 )
345
346 # Note: need to copy _dispatch to prevent instances of the builtin
347 # PrettyPrinter class to call methods of _EstimatorPrettyPrinter (see issue
348 # 12906)
349 # mypy error: "Type[PrettyPrinter]" has no attribute "_dispatch"
350 _dispatch = pprint.PrettyPrinter._dispatch.copy() # type: ignore[attr-defined]
351 _dispatch[BaseEstimator.__repr__] = _pprint_estimator
352 _dispatch[KeyValTuple.__repr__] = _pprint_key_val_tuple
353
354
355def _safe_repr(object, context, maxlevels, level, changed_only=False):
356 """Same as the builtin _safe_repr, with added support for Estimator
357 objects."""
358 typ = type(object)
359
360 if typ in pprint._builtin_scalars:
361 return repr(object), True, False
362
363 r = getattr(typ, "__repr__", None)
364 if issubclass(typ, dict) and r is dict.__repr__:
365 if not object:
366 return "{}", True, False
367 objid = id(object)
368 if maxlevels and level >= maxlevels:
369 return "{...}", False, objid in context
370 if objid in context:
371 return pprint._recursion(object), False, True
372 context[objid] = 1
373 readable = True
374 recursive = False
375 components = []
376 append = components.append
377 level += 1
378 saferepr = _safe_repr
379 items = sorted(object.items(), key=pprint._safe_tuple)
380 for k, v in items:
381 krepr, kreadable, krecur = saferepr(
382 k, context, maxlevels, level, changed_only=changed_only
383 )
384 vrepr, vreadable, vrecur = saferepr(
385 v, context, maxlevels, level, changed_only=changed_only
386 )
387 append("%s: %s" % (krepr, vrepr))
388 readable = readable and kreadable and vreadable
389 if krecur or vrecur:
390 recursive = True
391 del context[objid]
392 return "{%s}" % ", ".join(components), readable, recursive
393
394 if (issubclass(typ, list) and r is list.__repr__) or (
395 issubclass(typ, tuple) and r is tuple.__repr__
396 ):
397 if issubclass(typ, list):
398 if not object:
399 return "[]", True, False
400 format = "[%s]"
401 elif len(object) == 1:
402 format = "(%s,)"
403 else:
404 if not object:
405 return "()", True, False
406 format = "(%s)"
407 objid = id(object)
408 if maxlevels and level >= maxlevels:
409 return format % "...", False, objid in context
410 if objid in context:
411 return pprint._recursion(object), False, True
412 context[objid] = 1
413 readable = True
414 recursive = False
415 components = []
416 append = components.append
417 level += 1
418 for o in object:
419 orepr, oreadable, orecur = _safe_repr(
420 o, context, maxlevels, level, changed_only=changed_only
421 )
422 append(orepr)
423 if not oreadable:
424 readable = False
425 if orecur:
426 recursive = True
427 del context[objid]
428 return format % ", ".join(components), readable, recursive
429
430 if issubclass(typ, BaseEstimator):
431 objid = id(object)
432 if maxlevels and level >= maxlevels:
433 return f"{typ.__name__}(...)", False, objid in context
434 if objid in context:
435 return pprint._recursion(object), False, True
436 context[objid] = 1
437 readable = True
438 recursive = False
439 if changed_only:
440 params = _changed_params(object)
441 else:
442 params = object.get_params(deep=False)
443 components = []
444 append = components.append
445 level += 1
446 saferepr = _safe_repr
447 items = sorted(params.items(), key=pprint._safe_tuple)
448 for k, v in items:
449 krepr, kreadable, krecur = saferepr(
450 k, context, maxlevels, level, changed_only=changed_only
451 )
452 vrepr, vreadable, vrecur = saferepr(
453 v, context, maxlevels, level, changed_only=changed_only
454 )
455 append("%s=%s" % (krepr.strip("'"), vrepr))
456 readable = readable and kreadable and vreadable
457 if krecur or vrecur:
458 recursive = True
459 del context[objid]
460 return ("%s(%s)" % (typ.__name__, ", ".join(components)), readable, recursive)
461
462 rep = repr(object)
463 return rep, (rep and not rep.startswith("<")), False
464 