Skip to content

Subclassing PrettyPrinter.format doesn't work in all cases #132855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
gsnedders opened this issue Apr 23, 2025 · 6 comments
Open

Subclassing PrettyPrinter.format doesn't work in all cases #132855

gsnedders opened this issue Apr 23, 2025 · 6 comments
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@gsnedders
Copy link
Contributor

gsnedders commented Apr 23, 2025

Bug report

Bug description:

See also the earlier fixed #73036. This appears to have only been a partial fix, or it has regressed.

import collections
import pprint
import types


SENTINAL = object()


class _hashable:
    def __hash__(self):
        return 1


HASHABLE_SENTINAL = _hashable()


class CustomPrettyPrinter(pprint.PrettyPrinter):
    def format(self, obj, context, maxlevels, level):
        if obj is SENTINAL:
            return "SENTINAL", True, False
        elif obj is HASHABLE_SENTINAL:
            return "HASHABLE_SENTINAL", True, False
        else:
            return super().format(obj, context, maxlevels, level)


if __name__ == "__main__":
    printer = CustomPrettyPrinter(sort_dicts=False)

    test_data = {
        "item": SENTINAL,
        "dict": {"a": SENTINAL},
        "dict_key": {HASHABLE_SENTINAL: 1},
        "OrderedDict": collections.OrderedDict({"a": SENTINAL}),
        "OrderedDict_key": collections.OrderedDict({HASHABLE_SENTINAL: 1}),
        "list": [SENTINAL],
        "tuple": (SENTINAL,),
        "set": {SENTINAL},
        "frozenset": frozenset({SENTINAL}),
        "mappingproxy": types.MappingProxyType({"a": SENTINAL}),
        "mappingproxy_key": types.MappingProxyType({HASHABLE_SENTINAL: 1}),
        "SimpleNamespace": types.SimpleNamespace(a=SENTINAL),
        "defaultdict": collections.defaultdict(list, {"a": SENTINAL}),
        "defaultdict_key": collections.defaultdict(list, {HASHABLE_SENTINAL: 1}),
        "Counter": collections.Counter({"a": SENTINAL}),
        "Counter_key": collections.Counter({HASHABLE_SENTINAL: 1}),
        "deque": collections.deque([SENTINAL]),
    }

    printer.pprint(test_data)

This outputs:

{'item': SENTINAL,
 'dict': {'a': SENTINAL},
 'dict_key': {HASHABLE_SENTINAL: 1},
 'OrderedDict': OrderedDict({'a': <object object at 0x1041f4660>}),
 'OrderedDict_key': OrderedDict([(HASHABLE_SENTINAL, 1)]),
 'list': [SENTINAL],
 'tuple': (SENTINAL,),
 'set': {<object object at 0x1041f4660>},
 'frozenset': frozenset({<object object at 0x1041f4660>}),
 'mappingproxy': mappingproxy({'a': <object object at 0x1041f4660>}),
 'mappingproxy_key': mappingproxy({HASHABLE_SENTINAL: 1}),
 'SimpleNamespace': namespace(a=<object object at 0x1041f4660>),
 'defaultdict': defaultdict(<class 'list'>,
                            {'a': SENTINAL}),
 'defaultdict_key': defaultdict(<class 'list'>,
                                {HASHABLE_SENTINAL: 1}),
 'Counter': Counter({'a': <object object at 0x1041f4660>}),
 'Counter_key': Counter({<__main__._hashable object at 0x1045e4d70>: 1}),
 'deque': deque([<object object at 0x1041f4660>])}

Every instance of < in that output is a bug.

CPython versions tested on:

3.13

Operating systems tested on:

macOS

Linked PRs

@gsnedders gsnedders added the type-bug An unexpected behavior, bug, or error label Apr 23, 2025
@gsnedders
Copy link
Contributor Author

For a little background about impact, I ran into this trying to do something like:

class JSONPrettyPrinter(pprint.PrettyPrinter):
    def __init__(self, json_encoder=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.json_encoder = json_encoder or json.JSONEncoder

    def format(self, obj, context, maxlevels, level):
        if isinstance(obj, (list, tuple, dict)):
            return super().format(obj, context, maxlevels, level)

        if (
            isinstance(obj, (str, int, float))
            or obj is None
            or obj is True
            or obj is False
        ):
            return self.json_encoder.encode(obj), True, False

        return self.format(self.json_encoder.default(obj), context, maxlevels, level)

This gives a bogus result when formatting something like {"set": {1, 2, 3}}, as this should call the json_encoder.default, but because it never actually calls format with the set object as an argument you have no way of doing this.

@ZeroIntensity ZeroIntensity added the stdlib Python modules in the Lib dir label Apr 24, 2025
@ZeroIntensity
Copy link
Member

Would you like to submit a PR?

@Agent-Hellboy
Copy link
Contributor

Agent-Hellboy commented Apr 30, 2025

Hi @ZeroIntensity , I can add a PR if she is not interested
Btw, I feel like instead of fixing _safe_repr and adding specific dispatch to _safe_repr we should dispatch custom repr here only

rep = self._repr(object, context, level)

and we can skip primitive type like _builtin_scalars

_builtin_scalars = frozenset({str, bytes, bytearray, float, complex,

we can call specific dispatcher here but i don't know if it's a thoughtful and reasonable choice

rep = repr(object)

@RyanP1978

This comment was marked as spam.

@gsnedders
Copy link
Contributor Author

I can add a PR if she is not interested

@Agent-Hellboy Go ahead! I'm not going to any time soon.

@Agent-Hellboy
Copy link
Contributor

@gsnedders added a PR, please review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants