Skip to content
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

PyPy-ready IPython.lib.pretty, new tox.ini and consistent dict/mapping proxies pretty printing #9827

Merged
merged 10 commits into from Aug 2, 2016
20 changes: 19 additions & 1 deletion .travis.yml
Expand Up @@ -6,12 +6,29 @@ python:
- 3.4
- 3.3
- 2.7
- pypy
sudo: false
before_install:
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
- 'if [[ $GROUP != js* ]]; then COVERAGE=""; fi'
install:
- pip install "setuptools>=18.5"
# Installs PyPy (+ its Numpy). Based on @frol comment at:
# https://github.com/travis-ci/travis-ci/issues/5027
- |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
export PYPY_VERSION="5.3.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
pip install https://bitbucket.org/pypy/numpy/get/master.zip
fi
- pip install -f travis-wheels/wheelhouse -e file://$PWD#egg=ipython[test]
- pip install codecov
script:
Expand All @@ -23,4 +40,5 @@ after_success:

matrix:
allow_failures:
python: nightly
- python: nightly
- python: pypy
26 changes: 19 additions & 7 deletions IPython/lib/pretty.py
Expand Up @@ -85,7 +85,7 @@ def _repr_pretty_(self, p, cycle):
import datetime
from collections import deque

from IPython.utils.py3compat import PY3, cast_unicode, string_types
from IPython.utils.py3compat import PY3, PYPY, cast_unicode, string_types
from IPython.utils.encoding import get_stream_enc

from io import StringIO
Expand Down Expand Up @@ -605,7 +605,8 @@ def inner(obj, p, cycle):

if cycle:
return p.text('{...}')
p.begin_group(1, start)
step = len(start)
p.begin_group(step, start)
keys = obj.keys()
# if dict isn't large enough to be truncated, sort keys before displaying
if not (p.max_seq_length and len(obj) >= p.max_seq_length):
Expand All @@ -621,7 +622,7 @@ def inner(obj, p, cycle):
p.pretty(key)
p.text(': ')
p.pretty(obj[key])
p.end_group(1, end)
p.end_group(step, end)
return inner


Expand All @@ -631,7 +632,11 @@ def _super_pprint(obj, p, cycle):
p.pretty(obj.__thisclass__)
p.text(',')
p.breakable()
p.pretty(obj.__self__)
if PYPY: # In PyPy, super() objects don't have __self__ attributes
dself = obj.__repr__.__self__
p.pretty(None if dself is obj else dself)
else:
p.pretty(obj.__self__)
p.end_group(8, '>')


Expand Down Expand Up @@ -665,8 +670,10 @@ def _type_pprint(obj, p, cycle):
# Heap allocated types might not have the module attribute,
# and others may set it to None.

# Checks for a __repr__ override in the metaclass
if type(obj).__repr__ is not type.__repr__:
# Checks for a __repr__ override in the metaclass. Can't compare the
# type(obj).__repr__ directly because in PyPy the representation function
# inherited from type isn't the same type.__repr__
if [m for m in _get_mro(type(obj)) if "__repr__" in vars(m)][:1] != [type]:
_repr_pprint(obj, p, cycle)
return

Expand Down Expand Up @@ -753,10 +760,15 @@ def _exception_pprint(obj, p, cycle):
}

try:
_type_pprinters[types.DictProxyType] = _dict_pprinter_factory('<dictproxy {', '}>')
# In PyPy, types.DictProxyType is dict, setting the dictproxy printer
# using dict.setdefault avoids overwritting the dict printer
_type_pprinters.setdefault(types.DictProxyType,
_dict_pprinter_factory('dict_proxy({', '})'))
_type_pprinters[types.ClassType] = _type_pprint
_type_pprinters[types.SliceType] = _repr_pprint
except AttributeError: # Python 3
_type_pprinters[types.MappingProxyType] = \
_dict_pprinter_factory('mappingproxy({', '})')
_type_pprinters[slice] = _repr_pprint

try:
Expand Down
104 changes: 101 additions & 3 deletions IPython/lib/tests/test_pretty.py
Expand Up @@ -7,11 +7,13 @@
from __future__ import print_function

from collections import Counter, defaultdict, deque, OrderedDict
import types, string, ctypes

import nose.tools as nt

from IPython.lib import pretty
from IPython.testing.decorators import skip_without, py2_only
from IPython.testing.decorators import (skip_without, py2_only, py3_only,
cpython2_only)
from IPython.utils.py3compat import PY3, unicode_to_str

if PY3:
Expand Down Expand Up @@ -186,12 +188,14 @@ class SB(SA):
pass

def test_super_repr():
# "<super: module_name.SA, None>"
output = pretty.pretty(super(SA))
nt.assert_in("SA", output)
nt.assert_regexp_matches(output, r"<super: \S+.SA, None>")

# "<super: module_name.SA, <module_name.SB at 0x...>>"
sb = SB()
output = pretty.pretty(super(SA, sb))
nt.assert_in("SA", output)
nt.assert_regexp_matches(output, r"<super: \S+.SA,\s+<\S+.SB at 0x\S+>>")


def test_long_list():
Expand Down Expand Up @@ -436,3 +440,97 @@ class MyCounter(Counter):
]
for obj, expected in cases:
nt.assert_equal(pretty.pretty(obj), expected)

@py3_only
def test_mappingproxy():
MP = types.MappingProxyType
underlying_dict = {}
mp_recursive = MP(underlying_dict)
underlying_dict[2] = mp_recursive
underlying_dict[3] = underlying_dict

cases = [
(MP({}), "mappingproxy({})"),
(MP({None: MP({})}), "mappingproxy({None: mappingproxy({})})"),
(MP({k: k.upper() for k in string.ascii_lowercase}),
"mappingproxy({'a': 'A',\n"
" 'b': 'B',\n"
" 'c': 'C',\n"
" 'd': 'D',\n"
" 'e': 'E',\n"
" 'f': 'F',\n"
" 'g': 'G',\n"
" 'h': 'H',\n"
" 'i': 'I',\n"
" 'j': 'J',\n"
" 'k': 'K',\n"
" 'l': 'L',\n"
" 'm': 'M',\n"
" 'n': 'N',\n"
" 'o': 'O',\n"
" 'p': 'P',\n"
" 'q': 'Q',\n"
" 'r': 'R',\n"
" 's': 'S',\n"
" 't': 'T',\n"
" 'u': 'U',\n"
" 'v': 'V',\n"
" 'w': 'W',\n"
" 'x': 'X',\n"
" 'y': 'Y',\n"
" 'z': 'Z'})"),
(mp_recursive, "mappingproxy({2: {...}, 3: {2: {...}, 3: {...}}})"),
(underlying_dict,
"{2: mappingproxy({2: {...}, 3: {...}}), 3: {...}}"),
]
for obj, expected in cases:
nt.assert_equal(pretty.pretty(obj), expected)

@cpython2_only # In PyPy, types.DictProxyType is dict
def test_dictproxy():
# This is the dictproxy constructor itself from the Python API,
DP = ctypes.pythonapi.PyDictProxy_New
DP.argtypes, DP.restype = (ctypes.py_object,), ctypes.py_object

underlying_dict = {}
mp_recursive = DP(underlying_dict)
underlying_dict[0] = mp_recursive
underlying_dict[-3] = underlying_dict

cases = [
(DP({}), "dict_proxy({})"),
(DP({None: DP({})}), "dict_proxy({None: dict_proxy({})})"),
(DP({k: k.lower() for k in string.ascii_uppercase}),
"dict_proxy({'A': 'a',\n"
" 'B': 'b',\n"
" 'C': 'c',\n"
" 'D': 'd',\n"
" 'E': 'e',\n"
" 'F': 'f',\n"
" 'G': 'g',\n"
" 'H': 'h',\n"
" 'I': 'i',\n"
" 'J': 'j',\n"
" 'K': 'k',\n"
" 'L': 'l',\n"
" 'M': 'm',\n"
" 'N': 'n',\n"
" 'O': 'o',\n"
" 'P': 'p',\n"
" 'Q': 'q',\n"
" 'R': 'r',\n"
" 'S': 's',\n"
" 'T': 't',\n"
" 'U': 'u',\n"
" 'V': 'v',\n"
" 'W': 'w',\n"
" 'X': 'x',\n"
" 'Y': 'y',\n"
" 'Z': 'z'})"),
(mp_recursive, "dict_proxy({-3: {-3: {...}, 0: {...}}, 0: {...}})"),
]
for obj, expected in cases:
nt.assert_is_instance(obj, types.DictProxyType) # Meta-test
nt.assert_equal(pretty.pretty(obj), expected)
nt.assert_equal(pretty.pretty(underlying_dict),
"{-3: {...}, 0: dict_proxy({-3: {...}, 0: {...}})}")
3 changes: 2 additions & 1 deletion IPython/testing/decorators.py
Expand Up @@ -48,7 +48,7 @@
from IPython.external.decorators import *

# For onlyif_cmd_exists decorator
from IPython.utils.py3compat import string_types, which, PY2, PY3
from IPython.utils.py3compat import string_types, which, PY2, PY3, PYPY

#-----------------------------------------------------------------------------
# Classes and functions
Expand Down Expand Up @@ -336,6 +336,7 @@ def skip_file_no_x11(name):
known_failure_py3 = knownfailureif(sys.version_info[0] >= 3,
'This test is known to fail on Python 3.')

cpython2_only = skipif(PY3 or PYPY, "This test only runs on CPython 2.")
py2_only = skipif(PY3, "This test only runs on Python 2.")
py3_only = skipif(PY2, "This test only runs on Python 3.")

Expand Down
2 changes: 2 additions & 0 deletions IPython/utils/py3compat.py
Expand Up @@ -6,6 +6,7 @@
import re
import shutil
import types
import platform

from .encoding import DEFAULT_ENCODING

Expand Down Expand Up @@ -292,6 +293,7 @@ def execfile(fname, glob=None, loc=None, compiler=None):


PY2 = not PY3
PYPY = platform.python_implementation() == "PyPy"


def annotate(**kwargs):
Expand Down
56 changes: 20 additions & 36 deletions tox.ini
@@ -1,44 +1,28 @@
# Tox (http://tox.testrun.org/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.

# Building the source distribution requires `invoke` and `lessc` to be on your PATH.
# "pip install invoke" will install invoke. Less can be installed by
# node.js' (http://nodejs.org/) package manager npm:
# "npm install -g less".

# Javascript tests need additional dependencies that can be installed
# using node.js' package manager npm:
# [*] casperjs: "npm install -g casperjs"
# [*] slimerjs: "npm install -g slimerjs"
# [*] phantomjs: "npm install -g phantomjs"

# Note: qt4 versions break some tests with tornado versions >=4.0.
; Tox (http://tox.testrun.org/) is a virtualenv manager for running tests in
; multiple environments. This configuration file gets the requirements from
; setup.py like a "pip install ipython[test]". To create the environments, it
; requires every interpreter available/installed.
; -- Commands --
; pip install tox # Installs tox
; tox # Runs the tests (call from the directory with tox.ini)
; tox -r # Ditto, but forcing the virtual environments to be rebuilt
; tox -e py35,pypy # Runs only in the selected environments
; tox -- --all -j # Runs "iptest --all -j" in every environment

[tox]
envlist = py27, py33, py34
envlist = py{36,35,34,33,27,py}
skip_missing_interpreters = True
toxworkdir = /tmp/tox_ipython

[testenv]
; PyPy requires its Numpy fork instead of "pip install numpy"
; Other IPython/testing dependencies should be in setup.py, not here
deps =
pyzmq
nose
tornado<4.0
jinja2
sphinx
pygments
jsonpointer
jsonschema
mistune
pypy: https://bitbucket.org/pypy/numpy/get/master.zip
py{36,35,34,33,27}: matplotlib
.[test]

# To avoid loading IPython module in the current directory, change
# current directory to ".tox/py*/tmp" before running test.
; It's just to avoid loading the IPython package in the current directory
changedir = {envtmpdir}

commands =
iptest --all

[testenv:py27]
deps=
mock
{[testenv]deps}
commands = iptest {posargs}