CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
_pprint.py464 linesDownload Raw Back to utils
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 
Aluode/PerceptionLabPortable · CoolFace