diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb50c2..8cf2c91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: env_vars: EXTRAS,PYTHON,OS - name: uninstall extras - run: pip uninstall -y multidict numpy pydantic + run: pip uninstall -y multidict numpy pydantic asyncpg - name: test without extras run: | diff --git a/devtools/prettier.py b/devtools/prettier.py index ce06a91..d6a1a13 100644 --- a/devtools/prettier.py +++ b/devtools/prettier.py @@ -3,7 +3,7 @@ from collections import OrderedDict from collections.abc import Generator -from .utils import LazyIsInstance, isatty +from .utils import isatty __all__ = 'PrettyFormat', 'pformat', 'pprint' MYPY = False @@ -18,7 +18,6 @@ DEFAULT_WIDTH = int(os.getenv('PY_DEVTOOLS_WIDTH', 120)) MISSING = object() PRETTY_KEY = '__prettier_formatted_value__' -MultiDict = LazyIsInstance['multidict', 'MultiDict'] def env_true(var_name, alt=None): @@ -70,7 +69,6 @@ def __init__( (tuple, self._format_tuples), ((list, set, frozenset), self._format_list_like), (Generator, self._format_generators), - (MultiDict, self._format_dict), ] def __call__(self, value: 'Any', *, indent: int = 0, indent_first: bool = False, highlight: bool = False): @@ -114,6 +112,14 @@ def _format(self, value: 'Any', indent_current: int, indent_first: bool): if isinstance(value, t): func(value, value_repr, indent_current, indent_new) return + + # very blunt check for things that look like dictionaries but do not necessarily inherit from Mapping + # e.g. asyncpg Records + # HELP: are there any other checks we should include here? + if hasattr(value, '__getitem__') and hasattr(value, 'items') and callable(value.items): + self._format_dict(value, value_repr, indent_current, indent_new) + return + self._format_raw(value, value_repr, indent_current, indent_new) def _render_pretty(self, gen, indent: int): @@ -136,13 +142,13 @@ def _render_pretty(self, gen, indent: int): # shouldn't happen but will self._stream.write(repr(v)) - def _format_dict(self, value: dict, value_repr: str, indent_current: int, indent_new: int): + def _format_dict(self, value: 'Any', value_repr: str, indent_current: int, indent_new: int): open_, before_, split_, after_, close_ = '{\n', indent_new * self._c, ': ', ',\n', '}' if isinstance(value, OrderedDict): open_, split_, after_, close_ = 'OrderedDict([\n', ', ', '),\n', '])' before_ += '(' - elif isinstance(value, MultiDict): - open_, close_ = '<{}(\n'.format(value.__class__.__name__), ')>' + elif not isinstance(value, dict): + open_, close_ = '<{}({{\n'.format(value.__class__.__name__), '})>' self._stream.write(open_) for k, v in value.items(): diff --git a/devtools/utils.py b/devtools/utils.py index 00a2de1..12ec9a8 100644 --- a/devtools/utils.py +++ b/devtools/utils.py @@ -1,7 +1,6 @@ -import importlib import sys -__all__ = ('isatty', 'LazyIsInstance') +__all__ = ('isatty',) def isatty(stream=None): @@ -10,29 +9,3 @@ def isatty(stream=None): return stream.isatty() except Exception: return False - - -class LazyIsInstanceMeta(type): - _package_path: str - _cls_name: str - _t = None - - def __instancecheck__(self, instance): - if self._t is None: - - try: - m = importlib.import_module(self._package_path) - except ImportError: - self._t = False - else: - self._t = getattr(m, self._cls_name) - - return self._t and isinstance(instance, self._t) - - def __getitem__(self, item): - package_path, cls_name = item - return type(cls_name, (self,), {'_package_path': package_path, '_cls_name': cls_name, '_t': None}) - - -class LazyIsInstance(metaclass=LazyIsInstanceMeta): - pass diff --git a/tests/requirements.txt b/tests/requirements.txt index e2eed0e..d6fd1ed 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -12,5 +12,6 @@ pytest-mock==3.1.0 pytest-sugar==0.9.3 pytest-toolbox==0.4 twine==3.1.1 +asyncpg # pyup: ignore numpy # pyup: ignore multidict # pyup: ignore diff --git a/tests/test_prettier.py b/tests/test_prettier.py index e657501..c86e457 100644 --- a/tests/test_prettier.py +++ b/tests/test_prettier.py @@ -1,3 +1,4 @@ +import os import string import sys from collections import OrderedDict, namedtuple @@ -19,6 +20,11 @@ CIMultiDict = None MultiDict = None +try: + from asyncpg.protocol.protocol import _create_record as Record +except ImportError: + Record = None + def test_dict(): v = pformat({1: 2, 3: 4}) @@ -269,20 +275,91 @@ def test_multidict(): d.add('b', 3) v = pformat(d) assert set(v.split('\n')) == { - "", + "})>", } -@pytest.mark.skipif(CIMultiDict is None, reason='CIMultiDict not installed') +@pytest.mark.skipif(CIMultiDict is None, reason='MultiDict not installed') def test_cimultidict(): v = pformat(CIMultiDict({'a': 1, 'b': 2})) assert set(v.split('\n')) == { - "", + "})>", } + + +def test_os_environ(): + v = pformat(os.environ) + assert v.startswith('<_Environ({') + assert " 'HOME': '" in v + + +class Foo: + a = 1 + + def __init__(self): + self.b = 2 + self.c = 3 + + +def test_dir(): + assert pformat(vars(Foo())) == ( + "{\n" + " 'b': 2,\n" + " 'c': 3,\n" + "}" + ) + + +def test_instance_dict(): + assert pformat(Foo().__dict__) == ( + "{\n" + " 'b': 2,\n" + " 'c': 3,\n" + "}" + ) + + +def test_class_dict(): + s = pformat(Foo.__dict__) + assert s.startswith('') + + +def test_dictlike(): + class Dictlike: + _d = {'x': 4, 'y': 42, 3: None} + + def items(self): + yield from self._d.items() + + def __getitem__(self, item): + return self._d[item] + + assert pformat(Dictlike()) == ( + "" + ) + + +@pytest.mark.skipif(Record is None, reason='asyncpg not installed') +def test_asyncpg_record(): + r = Record({'a': 0, 'b': 1}, (41, 42)) + assert dict(r) == {'a': 41, 'b': 42} + assert pformat(r) == ( + "" + )