diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..ae621f6 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,29 @@ +name: linting +# Linting tools use `pyproject.toml` and `setup.cfg` for config. + +on: + - pull_request + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.7" + - name: Install requirements + run: pip install -r requirements-dev.txt + - name: isort + run: isort . + - name: black + run: black . + - name: flake8 + run: flake8 . + - name: mypy + run: mypy . + - name: bandit + run: bandit . + - name: safety + run: safety check diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4ec498a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,26 @@ +name: tests + +on: + - pull_request + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.7'] + name: Python ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + - name: Install requirements + run: | + python -m pip install -U pip + python -m pip install -r requirements-test.txt + - name: Run tests + run: | + python -m pytest . diff --git a/.gitignore b/.gitignore index 541171f..d9005f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,152 @@ -*.class -*.o -*.pyc -*.sqlite3 -*.sw[op] -*~ -.DS_Store -bin-debug/* -bin-release/* -bin/* -tags -*.beam -*.dump -env/ -.env/ -*egg-info* -misc/ -dist/ -Icon? +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ .tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..68dd488 --- /dev/null +++ b/.python-version @@ -0,0 +1,4 @@ +3.7.12 +3.8.12 +3.9.10 +3.10.2 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index df6fc56..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python - -python: - - 2.7 - - 3.6 - - 3.5 - - 3.4 - - pypy - -install: - - pip install -r test-requires.txt - -script: - - python test.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index dbc81f1..2406b51 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +0.5.0 (unreleased) + * Remove support for older Python versions (3.7+ from now on). + * Add `sort_dict` option (thanks @gergelyk). + * Use `pytest` instead of `nose` (thanks @pgajdos). + * Use GitHub Actions instead of Travis. + * Security: `safety` and `bandit`. + 0.4.0 * Add IPython plugin (thanks @roee30; https://github.com/wolever/pprintpp/pull/20) diff --git a/README.rst b/README.rst index 15aaa71..cd3c1a1 100644 --- a/README.rst +++ b/README.rst @@ -1,17 +1,11 @@ ``pprint++``: a drop-in replacement for ``pprint`` that's actually pretty ========================================================================= -.. image:: https://travis-ci.org/wolever/pprintpp.svg?branch=master - :target: https://travis-ci.org/wolever/pprintpp - -Now with Python 3 support! - Installation ------------ -``pprint++`` can be installed with Python 2 or Python 3 using ``pip`` or -``easy_install``:: +``pprint++`` can be installed using ``pip`` or ``easy_install``:: $ pip install pprintpp - OR - @@ -42,14 +36,14 @@ Usage 3. As an `ipython `_ extension:: In [1]: %load_ext pprintpp - + This will use pprintpp for ipython's output. - + To load this extension when ipython starts, put the previous line in your `startup file `_. - + You can change the indentation level like so:: - - In [2]: %config PPrintPP.indentation = 4 + + In [2]: %config PPrintPP.indentation = 4 4. To monkeypatch ``pprint``:: @@ -218,7 +212,7 @@ Without ``printpp``:: >>> pprint.pprint(["Hello", np.array([[1,2],[3,4]])]) ['Hello', array([[1, 2], [3, 4]])] - >>> tweet = {'coordinates': None, 'created_at': 'Mon Jun 27 19:32:19 +0000 2011', 'entities': {'hashtags': [], 'urls': [{'display_url': 'tumblr.com/xnr37hf0yz', 'expanded_url': 'http://tumblr.com/xnr37hf0yz', 'indices': [107, 126], 'url': 'http://t.co/cCIWIwg'}], 'user_mentions': []}, 'place': None, 'source': 'Tumblr', 'truncated': False, 'user': {'contributors_enabled': True, 'default_profile': False, 'entities': {'hashtags': [], 'urls': [], 'user_mentions': []}, 'favourites_count': 20, 'id_str': '6253282', 'profile_link_color': '0094C2'}} + >>> tweet = {'coordinates': None, 'created_at': 'Mon Jun 27 19:32:19 +0000 2011', 'entities': {'hashtags': [], 'urls': [{'display_url': 'tumblr.com/xnr37hf0yz', 'expanded_url': 'http://tumblr.com/xnr37hf0yz', 'indices': [107, 126], 'url': 'http://t.co/cCIWIwg'}], 'user_mentions': []}, 'place': None, 'source': 'Tumblr', 'truncated': False, 'user': {'contributors_enabled': True, 'default_profile': False, 'entities': {'hashtags': [], 'urls': [], 'user_mentions': []}, 'favourites_count': 20, 'id_str': '6253282', 'profile_link_color': '0094C2'}} >>> pprint.pprint(tweet) {'coordinates': None, 'created_at': 'Mon Jun 27 19:32:19 +0000 2011', diff --git a/pp/README.rst b/pp/README.rst index 5dc130e..489778c 100644 --- a/pp/README.rst +++ b/pp/README.rst @@ -1,8 +1,7 @@ ``pp``: an alias for pprint++ ============================= -``pp`` can be installed with Python 2 or Python 3 using ``pip`` or -``easy_install``:: +``pp`` can be installed with using ``pip`` or ``easy_install``:: $ pip install pprintpp pp-ez - OR - diff --git a/pp/pp.py b/pp/pp.py index df25941..76859da 100644 --- a/pp/pp.py +++ b/pp/pp.py @@ -31,7 +31,7 @@ def fmt(self, *args, **kwargs): return self.pformat(*args, **kwargs) def __repr__(self): - return "" %( + return "" % ( self.__name__, self.pprint_mod.__name__, ) diff --git a/pp/setup.py b/pp/setup.py index 64b85d7..06e5fb8 100644 --- a/pp/setup.py +++ b/pp/setup.py @@ -14,7 +14,7 @@ setup( name="pp-ez", - version="0.2.0", + version="0.3.0", url="https://github.com/wolever/pprintpp", author="David Wolever", author_email="david@wolever.net", @@ -23,17 +23,21 @@ py_modules=["pp"], install_requires=[], license="BSD", - classifiers=[ x.strip() for x in """ - Development Status :: 3 - Alpha - Environment :: Console - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Natural Language :: English - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Topic :: Software Development - Topic :: Utilities - """.split("\n") if x.strip() ], + classifiers=[ + x.strip() + for x in """ + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Topic :: Software Development + Topic :: Utilities + """.split( + "\n" + ) + if x.strip() + ], ) diff --git a/pprintpp/__init__.py b/pprintpp/__init__.py index 7cb8b43..c1647a7 100644 --- a/pprintpp/__init__.py +++ b/pprintpp/__init__.py @@ -1,80 +1,28 @@ from __future__ import print_function +import ast import io import os -import ast import sys -import warnings import unicodedata +import warnings +from collections import Counter, OrderedDict, defaultdict -__all__ = [ - "pprint", "pformat", "isreadable", "isrecursive", "saferepr", - "PrettyPrinter", -] - - -# -# Py2/Py3 compatibility stuff -# - -try: - from collections import OrderedDict, defaultdict, Counter - _test_has_collections = True -except ImportError: - # Python 2.6 doesn't have collections - class dummy_class(object): - __repr__ = object() - OrderedDict = defaultdict = Counter = dummy_class - _test_has_collections = False - - -PY3 = sys.version_info >= (3, 0, 0) -BytesType = bytes -TextType = str if PY3 else unicode -u_prefix = '' if PY3 else 'u' - - -if PY3: - # Import builins explicitly to keep Py2 static analyzers happy - import builtins - chr_to_ascii = lambda x: builtins.ascii(x)[1:-1] - unichr = chr - from .safesort import safesort - _iteritems = lambda x: x.items() -else: - chr_to_ascii = lambda x: repr(x)[2:-1] - safesort = sorted - _iteritems = lambda x: x.iteritems() - - -def _sorted_py2(iterable): - with warnings.catch_warnings(): - if getattr(sys, "py3kwarning", False): - warnings.filterwarnings("ignore", "comparing unequal types " - "not supported", DeprecationWarning) - return sorted(iterable) +from .safesort import safesort + +__all__ = ["pprint", "pformat", "isreadable", "isrecursive", "saferepr", "PrettyPrinter"] + + +def chr_to_ascii(c): + return ascii(c)[1:-1] -def _sorted_py3(iterable): + +def _sorted(iterable): try: return sorted(iterable) except TypeError: return safesort(iterable) -_sorted = PY3 and _sorted_py3 or _sorted_py3 - -if hasattr(TextType, 'isascii'): # Python>=3.7 - _isascii = TextType.isascii -else: - def _isascii(text): - try: - text.encode('ascii') - except UnicodeEncodeError: - return False - return True - -# -# End compatibility stuff -# class TextIO(io.TextIOWrapper): def __init__(self, encoding=None): @@ -93,6 +41,7 @@ def getvalue(self): # ambiguous are repr'd, others will be printed. I made this table mostly by # hand, mostly guessing, so please file bugs. # Source: http://www.unicode.org/reports/tr44/#GC_Values_Table +# fmt: off unicode_printable_categories = { "Lu": 1, # Uppercase_Letter an uppercase letter "Ll": 1, # Lowercase_Letter a lowercase letter @@ -133,34 +82,41 @@ def getvalue(self): "Cn": 0, # Unassigned a reserved unassigned code point or a noncharacter "C": 0, # Other Cc | Cf | Cs | Co | Cn } +# fmt: on -ascii_table = dict( - (unichr(i), chr_to_ascii(unichr(i))) - for i in range(255) -) +ascii_table = dict((chr(i), chr_to_ascii(chr(i))) for i in range(255)) -def pprint(object, stream=None, indent=4, width=80, depth=None): + +def pprint(object, stream=None, indent=4, width=80, depth=None, sort_dicts=True): """Pretty-print a Python object to a stream [default is sys.stdout].""" printer = PrettyPrinter( - stream=stream, indent=indent, width=width, depth=depth) + stream=stream, indent=indent, width=width, depth=depth, sort_dicts=sort_dicts + ) printer.pprint(object) -def pformat(object, indent=4, width=80, depth=None): + +def pformat(object, indent=4, width=80, depth=None, sort_dicts=True): """Format a Python object into a pretty-printed representation.""" - return PrettyPrinter(indent=indent, width=width, depth=depth).pformat(object) + return PrettyPrinter(indent=indent, width=width, depth=depth, sort_dicts=sort_dicts).pformat( + object + ) + def saferepr(object): """Version of repr() which can handle recursive data structures.""" return PrettyPrinter().pformat(object) + def isreadable(object): """Determine if saferepr(object) is readable by eval().""" return PrettyPrinter().isreadable(object) + def isrecursive(object): """Determine if object requires a recursive representation.""" return PrettyPrinter().isrecursive(object) + def console(argv=None): if argv is None: argv = sys.argv @@ -168,22 +124,24 @@ def console(argv=None): name = argv[0] if name.startswith("/"): name = os.path.basename(name) - print("Usage: %s" %(argv[0], )) + print("Usage: %s" % (argv[0],)) print() - print("Pipe Python literals into %s to pretty-print them" %(argv[0], )) + print("Pipe Python literals into %s to pretty-print them" % (argv[0],)) return 1 obj = ast.literal_eval(sys.stdin.read().strip()) pprint(obj) return 0 + def monkeypatch(mod=None, quiet=False): if "pprint" in sys.modules and not quiet: - warnings.warn("'pprint' has already been imported; monkeypatching " - "won't work everywhere.") + warnings.warn("'pprint' has already been imported; monkeypatching won't work everywhere.") import pprint + sys.modules["pprint_original"] = pprint sys.modules["pprint"] = mod or sys.modules["pprintpp"] + class PPrintSharedState(object): recursive = False readable = True @@ -200,6 +158,7 @@ class PPrintState(object): level = 0 max_width = 80 max_depth = None + sort_dicts = True stream = None context = None write_constrain = None @@ -236,7 +195,7 @@ def write(self, data): if self.write_constrain < 0: raise self.WriteConstrained - if isinstance(data, BytesType): + if isinstance(data, bytes): data = data.decode("latin1") self.stream.write(data) nl_idx = data.rfind("\n") @@ -248,30 +207,34 @@ def write(self, data): def get_indent_string(self): return (self.level * self.indent) * " " + def _mk_open_close_empty_dict(type_tuples): - """ Generates a dictionary mapping either ``cls.__repr__`` xor ``cls`` to - a tuple of ``(container_type, repr_open, repr_close, repr_empty)`` (see - ``PrettyPrinter._open_close_empty`` for examples). + """ + Generates a dictionary mapping either ``cls.__repr__`` xor ``cls`` to + a tuple of ``(container_type, repr_open, repr_close, repr_empty)`` (see + ``PrettyPrinter._open_close_empty`` for examples). - Using either ``cls.__repr__`` xor ``cls`` is important because some - types (specifically, ``set`` and ``frozenset`` on PyPy) share a - ``__repr__``. When we are determining how to repr an object, the type - is first checked, then if it's not found ``type.__repr__`` is checked. + Using either ``cls.__repr__`` xor ``cls`` is important because some + types (specifically, ``set`` and ``frozenset`` on PyPy) share a + ``__repr__``. When we are determining how to repr an object, the type + is first checked, then if it's not found ``type.__repr__`` is checked. - Note that ``__repr__`` is used so that trivial subclasses will behave - sensibly. """ + Note that ``__repr__`` is used so that trivial subclasses will behave + sensibly.""" res = {} for (cls, open_close_empty) in type_tuples: if cls.__repr__ in res: - res[cls] = (cls, ) + open_close_empty + res[cls] = (cls,) + open_close_empty else: - res[cls.__repr__] = (cls, ) + open_close_empty + res[cls.__repr__] = (cls,) + open_close_empty return res + class PrettyPrinter(object): - def __init__(self, indent=4, width=80, depth=None, stream=None): - """Handle pretty printing operations onto a stream using a set of + def __init__(self, indent=4, width=80, depth=None, stream=None, sort_dicts=True): + """ + Handle pretty printing operations onto a stream using a set of configured parameters. indent @@ -287,10 +250,16 @@ def __init__(self, indent=4, width=80, depth=None, stream=None): The desired output stream. If omitted (or false), the standard output stream available at construction will be used. + sort_dicts + If `True`, dictionaries will be formatted with their keys sorted, + otherwise they will display in the order as returned by `items` + method. + """ self.get_default_state = lambda: PPrintState( indent=int(indent), max_width=int(width), + sort_dicts=sort_dicts, stream=stream or sys.stdout, context={}, ) @@ -318,16 +287,18 @@ def isreadable(self, object): self._format(object, state) return state.s.readable and not state.s.recursive - _open_close_empty = _mk_open_close_empty_dict([ - (dict, ("dict", "{", "}", "{}")), - (list, ("list", "[", "]", "[]")), - (tuple, ("tuple", "(", ")", "()")), - (set, ("set", "__PP_TYPE__([", "])", "__PP_TYPE__()")), - (frozenset, ("set", "__PP_TYPE__([", "])", "__PP_TYPE__()")), - (Counter, ("dict", "__PP_TYPE__({", "})", "__PP_TYPE__()")), - (defaultdict, ("dict", None, "})", None)), - (OrderedDict, ("odict", "__PP_TYPE__([", "])", "__PP_TYPE__()")), - ]) + _open_close_empty = _mk_open_close_empty_dict( + [ + (dict, ("dict", "{", "}", "{}")), + (list, ("list", "[", "]", "[]")), + (tuple, ("tuple", "(", ")", "()")), + (set, ("set", "__PP_TYPE__([", "])", "__PP_TYPE__()")), + (frozenset, ("set", "__PP_TYPE__([", "])", "__PP_TYPE__()")), + (Counter, ("dict", "__PP_TYPE__({", "})", "__PP_TYPE__()")), + (defaultdict, ("dict", None, "})", None)), + (OrderedDict, ("odict", "__PP_TYPE__([", "])", "__PP_TYPE__()")), + ] + ) def _format_nested_objects(self, object, state, typeish=None): objid = id(object) @@ -338,12 +309,9 @@ def _format_nested_objects(self, object, state, typeish=None): # that it takes three characters to close the object (ex, `]),`) oneline_state = state.clone(clone_shared=True) oneline_state.stream = TextIO() - oneline_state.write_constrain = ( - state.max_width - state.s.cur_line_length - 3 - ) + oneline_state.write_constrain = state.max_width - state.s.cur_line_length - 3 try: - self._write_nested_real(object, oneline_state, typeish, - oneline=True) + self._write_nested_real(object, oneline_state, typeish, oneline=True) oneline_value = oneline_state.stream.getvalue() if "\n" in oneline_value: oneline_value = None @@ -363,7 +331,11 @@ def _write_nested_real(self, object, state, typeish, oneline=False): first = True joiner = oneline and ", " or ",\n" + indent_str if typeish == "dict": - for k, v in _sorted(object.items()): + items = object.items() + if state.sort_dicts: + items = _sorted(items) + + for k, v in items: if first: first = False else: @@ -372,7 +344,7 @@ def _write_nested_real(self, object, state, typeish, oneline=False): state.write(": ") self._format(v, state) elif typeish == "odict": - for k, v in _iteritems(object): + for k, v in object.items(): if first: first = False else: @@ -412,10 +384,7 @@ def _format(self, object, state): # Note: see comments on _mk_open_close_empty_dict for the rational # behind looking up based first on type then on __repr__. try: - opener_closer_empty = ( - self._open_close_empty.get(typ) or - self._open_close_empty.get(r) - ) + opener_closer_empty = self._open_close_empty.get(typ) or self._open_close_empty.get(r) except TypeError: # This will happen if the type or the __repr__ is unhashable. # See: https://github.com/wolever/pprintpp/issues/18 @@ -428,11 +397,11 @@ def _format(self, object, state): opener = "__PP_TYPE__(" + opener closer = closer + ")" if empty is not None and "__PP_TYPE__" not in empty: - empty = "__PP_TYPE__(%s)" %(empty, ) + empty = "__PP_TYPE__(%s)" % (empty,) if r == defaultdict.__repr__: factory_repr = object.default_factory - opener = "__PP_TYPE__(%r, {" %(factory_repr, ) + opener = "__PP_TYPE__(%r, {" % (factory_repr,) empty = opener + closer length = len(object) @@ -449,12 +418,12 @@ def _format(self, object, state): write(closer) return - if r == BytesType.__repr__: + if r == bytes.__repr__: write(repr(object)) return - if r == TextType.__repr__: - if _isascii(object): # Optimalization + if r == str.__repr__: + if str.isascii(object): # Optimalization write(repr(object)) return if "'" in object and '"' not in object: @@ -466,7 +435,7 @@ def _format(self, object, state): qget = quotes.get ascii_table_get = ascii_table.get unicat_get = unicodedata.category - write(u_prefix + quote) + write(quote) for char in object: if ord(char) > 0x7F: cat = unicat_get(char) @@ -476,26 +445,18 @@ def _format(self, object, state): continue except UnicodeEncodeError: pass - write( - qget(char) or - ascii_table_get(char) or - chr_to_ascii(char) - ) + write(qget(char) or ascii_table_get(char) or chr_to_ascii(char)) write(quote) return orepr = repr(object) orepr = orepr.replace("\n", "\n" + state.get_indent_string()) - state.s.readable = ( - state.s.readable and - not orepr.startswith("<") - ) + state.s.readable = state.s.readable and not orepr.startswith("<") write(orepr) return def _repr(self, object, context, level): - repr, readable, recursive = self.format(object, context.copy(), - self._depth, level) + repr, readable, recursive = self.format(object, context.copy(), self._depth, level) if not readable: self._readable = False if recursive: @@ -513,59 +474,63 @@ def format(self, object, context, maxlevels, level): def _recursion(self, object, state): state.s.recursive = True - return ("" - % (type(object).__name__, id(object))) + return "" % (type(object).__name__, id(object)) if __name__ == "__main__": try: import numpy as np except ImportError: - class np(object): + + class np(object): # type: ignore @staticmethod def array(o): return o - somelist = [1,2,3] - recursive = [] + somelist = [1, 2, 3] + recursive = [] # type: ignore # TODO: Add type hinting for mypy recursive.extend([recursive, recursive, recursive]) - pprint({ - "a": {"a": "b"}, - "b": [somelist, somelist], - "c": [ - (1, ), - (1,2,3), - ], - "ordereddict": OrderedDict([ - (1, 1), - (10, 10), - (2, 2), - (11, 11) - ]), - "counter": [ - Counter(), - Counter("asdfasdfasdf"), - ], - "dd": [ - defaultdict(int, {}), - defaultdict(int, {"foo": 42}), - ], - "frozenset": frozenset("abc"), - "np": [ - "hello", - #np.array([[1,2],[3,4]]), - "world", - ], - u"u": ["a", u"\u1234", "b"], - "recursive": recursive, - "z": { - "very very very long key stuff 1234": { - "much value": "very nest! " * 10, - u"unicode": u"4U!'\"", + pprint( + { + "a": {"a": "b"}, + "b": [somelist, somelist], + "c": [ + (1,), + (1, 2, 3), + ], + "ordereddict": OrderedDict( + [ + (1, 1), + (10, 10), + (2, 2), + (11, 11), + ] + ), + "counter": [ + Counter(), + Counter("asdfasdfasdf"), + ], + "dd": [ + defaultdict(int, {}), + defaultdict(int, {"foo": 42}), + ], + "frozenset": frozenset("abc"), + "np": [ + "hello", + # np.array([[1,2],[3,4]]), + "world", + ], + u"u": ["a", u"\u1234", "b"], + "recursive": recursive, + "z": { + "very very very long key stuff 1234": { + "much value": "very nest! " * 10, + u"unicode": u"4U!'\"", + }, + "aldksfj alskfj askfjas fkjasdlkf jasdlkf ajslfjas": ["asdf"] * 10, }, - "aldksfj alskfj askfjas fkjasdlkf jasdlkf ajslfjas": ["asdf"] * 10, - }, - }) + } + ) pprint(u"\xe9e\u0301") uni_safe = u"\xe9 \u6f02 \u0e4f \u2661" uni_unsafe = u"\u200a \u0301 \n" @@ -579,9 +544,11 @@ def array(o): def load_ipython_extension(ipython): from .ipython import load_ipython_extension + return load_ipython_extension(ipython) def unload_ipython_extension(ipython): from .ipython import unload_ipython_extension + return unload_ipython_extension(ipython) diff --git a/pprintpp/ipython.py b/pprintpp/ipython.py index 66f8477..fff3d77 100644 --- a/pprintpp/ipython.py +++ b/pprintpp/ipython.py @@ -9,12 +9,11 @@ https://stackoverflow.com/users/1530134/kupiakos """ import IPython -from traitlets.config import Configurable from traitlets import Int +from traitlets.config import Configurable from . import pformat - original_representation = IPython.lib.pretty.RepresentationPrinter DEFAULT_INDENTATION = 2 @@ -30,7 +29,8 @@ def unload_ipython_extension(ipython): IPython.lib.pretty.RepresentationPrinter = original_representation try: pprintpp = [ - configurable for configurable in ipython.configurables + configurable + for configurable in ipython.configurables if isinstance(configurable, PPrintPP) ][0] except IndexError: @@ -60,4 +60,5 @@ class PPrintPP(Configurable): """ PPrintPP configuration """ + indentation = Int(config=True) diff --git a/pprintpp/safesort.py b/pprintpp/safesort.py index a7631db..a5ddd5e 100644 --- a/pprintpp/safesort.py +++ b/pprintpp/safesort.py @@ -1,8 +1,6 @@ -import sys -import textwrap import functools +import textwrap -PY3 = (sys.version_info >= (3, 0, 0)) def memoized_property(f): @functools.wraps(f) @@ -10,10 +8,13 @@ def memoized_property_helper(self): val = f(self) self.__dict__[f.__name__] = val return val + return property(memoized_property_helper) + def _build_safe_cmp_func(name, cmp, prefix=""): - code = textwrap.dedent("""\ + code = textwrap.dedent( + f"""\ def {name}(self, other): try: return {prefix}(self.obj {cmp} other.obj) @@ -24,23 +25,20 @@ def {name}(self, other): except TypeError: pass return {prefix}(self.verysafeobj {cmp} other.verysafeobj) - """).format(name=name, cmp=cmp, prefix=prefix) + """ + ) gs = ls = {} exec(code, gs, ls) return gs[name] + class SafelySortable(object): def __init__(self, obj, key=None): - self.obj = ( - obj if key is None else - key(obj) - ) + self.obj = obj if key is None else key(obj) @memoized_property def prefix(self): - if PY3: - return tuple(t.__name__ for t in type(self.obj).__mro__) - return type(self.obj).__mro__ + return tuple(t.__name__ for t in type(self.obj).__mro__) @memoized_property def safeobj(self): @@ -68,6 +66,5 @@ def __hash__(self): def safesort(input, key=None, reverse=False): - """ Safely sort heterogeneous collections. """ - # TODO: support cmp= on Py 2.x? + """Safely sort heterogeneous collections.""" return sorted(input, key=lambda o: SafelySortable(o, key=key), reverse=reverse) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7aa7c75 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.bandit] +recursive = true # TODO: fix + +[tool.black] +line-length = 100 +target-version = ['py37'] +skip-string-normalization = true + +[tool.isort] +known_first_party = 'pprintpp' +profile = 'black' +sections = 'FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER' + +[tool.pytest.ini_options] +testpaths = ['tests'] + +[tool.mypy] +python_version = '3.7' +warn_return_any = true +warn_unused_configs = true +exclude = ['pp/setup.py'] + +[[tool.mypy.overrides]] +module = ['IPython', 'numpy', 'traitlets.*'] +ignore_missing_imports = true diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..55716fe --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,8 @@ +-r requirements-test.txt +bandit # Always want latest +black==22.3.0 +flake8==4.0.1 +isort==5.10.1 +mypy==0.942 +safety # Always want latest +types-setuptools == 57.4.12 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..49435c9 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1 @@ +pytest==7.1.1 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 2a9acf1..a672378 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,7 @@ [bdist_wheel] universal = 1 + +[flake8] +exclude = .git,.github,.venv,__pycache__,env/ +ignore = E261,E241 +max-line-length = 100 diff --git a/setup.py b/setup.py index aaece26..8761ea0 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name="pprintpp", - version="0.4.0", + version="0.5.0", url="https://github.com/wolever/pprintpp", author="David Wolever", author_email="david@wolever.net", @@ -28,17 +28,21 @@ }, install_requires=[], license="BSD", - classifiers=[ x.strip() for x in """ - Development Status :: 3 - Alpha - Environment :: Console - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Natural Language :: English - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Topic :: Software Development - Topic :: Utilities - """.split("\n") if x.strip() ], + classifiers=[ + x.strip() + for x in """ + Environment :: Console + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Natural Language :: English + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Topic :: Software Development + Topic :: Utilities + """.split( + "\n" + ) + if x.strip() + ], ) diff --git a/test-requires.txt b/test-requires.txt deleted file mode 100644 index 3621aa6..0000000 --- a/test-requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -nose==1.3.0 -parameterized==0.6.1 diff --git a/test.py b/test.py deleted file mode 100644 index 45a0075..0000000 --- a/test.py +++ /dev/null @@ -1,173 +0,0 @@ -from __future__ import print_function - -import sys -import ctypes -import textwrap - -from nose.tools import assert_equal -from parameterized import parameterized, param - -sys.path.append("pp/") -import pp -import pprintpp as p -from pprintpp import Counter, defaultdict, OrderedDict - -class PPrintppTestBase(object): - def assertStdout(self, expected, trim=True): - if trim: - expected = textwrap.dedent(expected.rstrip().lstrip("\n")) - # Assumes that nose's capture plugin is active - assert_equal(sys.stdout.getvalue().rstrip(), expected) - - -class TestPP(PPrintppTestBase): - def test_pp(self): - pp(["hello", "world"]) - self.assertStdout("['hello', 'world']") - - def test_pp_pprint(self): - pp.pprint("stuff") - self.assertStdout("'stuff'") - - def test_fmt(self): - print(pp.pformat("asdf")) - print(pp.fmt("stuff")) - self.assertStdout(""" - 'asdf' - 'stuff' - """) - - def test_module_like(self): - print(dir(pp)) - print(repr(pp)) - - -class MyDict(dict): - pass - -class MyList(list): - pass - -class MyTuple(tuple): - pass - -class MySet(set): - pass - -class MyFrozenSet(frozenset): - pass - -class MyOrderedDict(p.OrderedDict): - pass - -class MyDefaultDict(p.defaultdict): - pass - -class MyCounter(p.Counter): - pass - -class MyCounterWithRepr(p.Counter): - def __repr__(self): - return "MyCounterWithRepr('dummy')" - -class TestPPrint(PPrintppTestBase): - uni_safe = u"\xe9 \u6f02 \u0e4f \u2661" - uni_unsafe = u"\u200a \u0302 \n" - slashed = lambda s: u"%s'%s'" %( - p.u_prefix, - s.encode("ascii", "backslashreplace").decode("ascii").replace("\n", "\\n") - ) - - @parameterized([ - param("safe", uni_safe, "%s'%s'" %(p.u_prefix, uni_safe)), - param("unsafe", uni_unsafe, slashed(uni_unsafe)), - param("encoding-aware", uni_safe, slashed(uni_safe), encoding="ascii"), - param("high-end-chars", u"\U0002F9B2", slashed(u"\U0002F9B2"), encoding="ascii"), - ]) - def test_unicode(self, name, input, expected, encoding="utf-8"): - stream = p.TextIO(encoding=encoding) - p.pprint(input, stream=stream) - assert_equal(stream.getvalue().rstrip("\n"), expected) - - @parameterized([ - param(u"'\\'\"'"), - param(u'"\'"'), - param(u"'\"'"), - param("frozenset(['a', 'b', 'c'])"), - param("set([None, 1, 'a'])"), - - param("[]"), - param("[1]"), - param("{}"), - param("{1: 1}"), - param("set()"), - param("set([1])"), - param("frozenset()"), - param("frozenset([1])"), - param("()"), - param("(1, )"), - - param("MyDict({})"), - param("MyDict({1: 1})"), - param("MyList([])"), - param("MyList([1])"), - param("MyTuple(())"), - param("MyTuple((1, ))"), - param("MySet()"), - param("MySet([1])"), - param("MyFrozenSet()"), - param("MyFrozenSet([1])"), - - ] + ([] if not p._test_has_collections else [ - param("Counter()"), - param("Counter({1: 1})"), - param("OrderedDict()"), - param("OrderedDict([(1, 1), (5, 5), (2, 2)])"), - param("MyOrderedDict()"), - param("MyOrderedDict([(1, 1)])"), - param("MyCounter()"), - param("MyCounter({1: 1})"), - param("MyCounterWithRepr('dummy')"), - ])) - def test_back_and_forth(self, expected): - input = eval(expected) - stream = p.TextIO() - p.pprint(input, stream=stream) - assert_equal(stream.getvalue().rstrip("\n"), expected) - - if p._test_has_collections: - @parameterized([ - param("defaultdict(%r, {})" %(int, ), defaultdict(int)), - param("defaultdict(%r, {1: 1})" %(int, ), defaultdict(int, [(1, 1)])), - param("MyDefaultDict(%r, {})" %(int, ), MyDefaultDict(int)), - param("MyDefaultDict(%r, {1: 1})" %(int, ), MyDefaultDict(int, [(1, 1)])), - ]) - def test_expected_input(self, expected, input): - stream = p.TextIO() - p.pprint(input, stream=stream) - assert_equal(stream.getvalue().rstrip("\n"), expected) - - def test_unhashable_repr(self): - # In Python 3, C extensions can define a __repr__ method which is an - # instance of `instancemethod`, which is unhashable. It turns out to be - # spectacularly difficult to create an `instancemethod` and attach it to - # a type without using C... so we'll simulate it using a more explicitly - # unhashable type. - # See also: http://stackoverflow.com/q/40876368/71522 - - class UnhashableCallable(object): - __hash__ = None - - def __call__(self): - return "some-repr" - - class MyCls(object): - __repr__ = UnhashableCallable() - - obj = MyCls() - assert_equal(p.pformat(obj), "some-repr") - - -if __name__ == "__main__": - import nose - nose.main() diff --git a/tests/test_pprintpp.py b/tests/test_pprintpp.py new file mode 100644 index 0000000..3f346b3 --- /dev/null +++ b/tests/test_pprintpp.py @@ -0,0 +1,187 @@ +from __future__ import print_function + +import io +import sys +from contextlib import redirect_stdout +from collections import Counter, defaultdict, OrderedDict + +import pytest +import pprintpp as p + +sys.path.append("pp/") +import pp # noqa: E402 module level import not at top of file + + +def test_pp(): + expected = "['hello', 'world']" + f = io.StringIO() + with redirect_stdout(f): + pp(["hello", "world"]) + actual = f.getvalue().rstrip("\n") + assert actual == expected + + +def test_pp_print(): + expected = "'stuff'" + f = io.StringIO() + with redirect_stdout(f): + pp.pprint("stuff") + actual = f.getvalue().rstrip("\n") + assert actual == expected + + +def test_fmt(): + expected = "'asdf'\n'stuff'" + f = io.StringIO() + with redirect_stdout(f): + print(pp.pformat("asdf")) + print(pp.fmt("stuff")) + actual = f.getvalue().rstrip("\n") + assert actual == expected + + +def test_module_like(): + print(dir(pp)) + print(repr(pp)) + + +uni_safe = "\xe9 \u6f02 \u0e4f \u2661" +uni_unsafe = "\u200a \u0302 \n" + + +def slashed(s): + return "'%s'" % s.encode("ascii", "backslashreplace").decode("ascii").replace("\n", "\\n") + + +@pytest.mark.parametrize( + "input,expected,encoding", + [ + (uni_safe, "'%s'" % uni_safe, "utf-8"), + (uni_unsafe, slashed(uni_unsafe), "utf-8"), + (uni_unsafe, slashed(uni_unsafe), "ascii"), + ("\U0002F9B2", slashed("\U0002F9B2"), "ascii"), + ], +) +def test_unicode(input, expected, encoding): + stream = p.TextIO(encoding=encoding) + p.pprint(input, stream=stream) + assert stream.getvalue().rstrip("\n") == expected + + +test_back_and_forth_data = [ + "'\\'\"'", + '"\'"', + "'\"'", + "frozenset(['a', 'b', 'c'])", + "set([None, 1, 'a'])", + "[]", + "[1]", + "{}", + "{1: 1}", + "set()", + "set([1])", + "frozenset()", + "frozenset([1])", + "()", + "(1, )", + "MyDict({})", + "MyDict({1: 1})", + "MyList([])", + "MyList([1])", + "MyTuple(())", + "MyTuple((1, ))", + "MySet()", + "MySet([1])", + "MyFrozenSet()", + "MyFrozenSet([1])", + "Counter()", + "Counter({1: 1})", + "OrderedDict()", + "OrderedDict([(1, 1), (5, 5), (2, 2)])", + "MyOrderedDict()", + "MyOrderedDict([(1, 1)])", + "MyCounter()", + "MyCounter({1: 1})", + "MyCounterWithRepr('dummy')", +] + + +class MyDict(dict): + pass + + +class MyList(list): + pass + + +class MyTuple(tuple): + pass + + +class MySet(set): + pass + + +class MyFrozenSet(frozenset): + pass + + +class MyOrderedDict(OrderedDict): + pass + + +class MyDefaultDict(defaultdict): + pass + + +class MyCounter(Counter): + pass + + +class MyCounterWithRepr(p.Counter): + def __repr__(self): + return "MyCounterWithRepr('dummy')" + + +@pytest.mark.parametrize("expected", test_back_and_forth_data) +def test_back_and_forth(expected): + input = eval(expected) + stream = p.TextIO() + p.pprint(input, stream=stream) + assert stream.getvalue().rstrip("\n") == expected + + +@pytest.mark.parametrize( + "expected,input", + [ + ("defaultdict(%r, {})" % (int,), defaultdict(int)), + ("defaultdict(%r, {1: 1})" % (int,), defaultdict(int, [(1, 1)])), + ("MyDefaultDict(%r, {})" % (int,), MyDefaultDict(int)), + ("MyDefaultDict(%r, {1: 1})" % (int,), MyDefaultDict(int, [(1, 1)])), + ], +) +def test_expected_input(expected, input): + stream = p.TextIO() + p.pprint(input, stream=stream) + assert stream.getvalue().rstrip("\n") == expected + + +def test_unhashable_repr(): + # In Python 3, C extensions can define a __repr__ method which is an + # instance of `instancemethod`, which is unhashable. It turns out to be + # spectacularly difficult to create an `instancemethod` and attach it to + # a type without using C... so we'll simulate it using a more explicitly + # unhashable type. + # See also: http://stackoverflow.com/q/40876368/71522 + + class UnhashableCallable(object): + __hash__ = None + + def __call__(self): + return "some-repr" + + class MyCls(object): + __repr__ = UnhashableCallable() + + obj = MyCls() + assert p.pformat(obj) == "some-repr" diff --git a/tox.ini b/tox.ini index e194976..fac37d7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] -envlist = py26,py27,py33,py35,pypy +envlist = py37,py38,py39,py310,pypy [testenv] deps = - -rtest-requires.txt + -rrequirements-test.txt commands = - python test.py + python -m pytest . \ No newline at end of file