diff --git a/.travis.yml b/.travis.yml index b7e3d12..47d42d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: @@ -23,4 +40,5 @@ after_success: matrix: allow_failures: - python: nightly + - python: nightly + - python: pypy diff --git a/IPython/lib/pretty.py b/IPython/lib/pretty.py index 49ee2ce..97529f1 100644 --- a/IPython/lib/pretty.py +++ b/IPython/lib/pretty.py @@ -85,7 +85,7 @@ import re 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 @@ -605,7 +605,8 @@ def _dict_pprinter_factory(start, end, basetype=None): 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): @@ -621,7 +622,7 @@ def _dict_pprinter_factory(start, end, basetype=None): p.pretty(key) p.text(': ') p.pretty(obj[key]) - p.end_group(1, end) + p.end_group(step, end) return inner @@ -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, '>') @@ -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 @@ -753,10 +760,15 @@ _type_pprinters = { } try: - _type_pprinters[types.DictProxyType] = _dict_pprinter_factory('') + # 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: diff --git a/IPython/lib/tests/test_pretty.py b/IPython/lib/tests/test_pretty.py index 6995495..6f11e99 100644 --- a/IPython/lib/tests/test_pretty.py +++ b/IPython/lib/tests/test_pretty.py @@ -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: @@ -186,12 +188,14 @@ class SB(SA): pass def test_super_repr(): + # "" output = pretty.pretty(super(SA)) - nt.assert_in("SA", output) + nt.assert_regexp_matches(output, r"") + # ">" sb = SB() output = pretty.pretty(super(SA, sb)) - nt.assert_in("SA", output) + nt.assert_regexp_matches(output, r">") def test_long_list(): @@ -436,3 +440,97 @@ def test_collections_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: {...}})}") diff --git a/IPython/testing/decorators.py b/IPython/testing/decorators.py index a337254..087555d 100644 --- a/IPython/testing/decorators.py +++ b/IPython/testing/decorators.py @@ -48,7 +48,7 @@ from .ipunittest import ipdoctest, ipdocstring 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 @@ -336,6 +336,7 @@ skip_known_failure = knownfailureif(True,'This test is known to fail') 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.") diff --git a/IPython/utils/py3compat.py b/IPython/utils/py3compat.py index f42f55c..88602e5 100644 --- a/IPython/utils/py3compat.py +++ b/IPython/utils/py3compat.py @@ -6,6 +6,7 @@ import sys import re import shutil import types +import platform from .encoding import DEFAULT_ENCODING @@ -292,6 +293,7 @@ else: PY2 = not PY3 +PYPY = platform.python_implementation() == "PyPy" def annotate(**kwargs): diff --git a/tox.ini b/tox.ini index 3a19738..1668ce6 100644 --- a/tox.ini +++ b/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}