Skip to content

Commit 57e9836

Browse files
committed
Added publish to dataframe repr
1 parent c97cbc5 commit 57e9836

File tree

9 files changed

+155
-13
lines changed

9 files changed

+155
-13
lines changed

ci/requirements-2.7.pip

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ pathlib
77
backports.lzma
88
py
99
PyCrypto
10+
mock
11+
ipython

ci/requirements-3.5.run

+1
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ pymysql
1818
psycopg2
1919
s3fs
2020
beautifulsoup4
21+
ipython

ci/requirements-3.6.run

+1
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ pymysql
1818
beautifulsoup4
1919
s3fs
2020
xarray
21+
ipython

doc/source/options.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,9 @@ display.width 80 Width of the display in characters.
392392
IPython qtconsole, or IDLE do not run in a
393393
terminal and hence it is not possible
394394
to correctly detect the width.
395+
display.html.table_schema True Whether to publish a Table Schema
396+
representation for frontends that
397+
support it.
395398
html.border 1 A ``border=value`` attribute is
396399
inserted in the ``<table>`` tag
397400
for the DataFrame HTML repr.
@@ -516,7 +519,7 @@ Table Schema Display
516519
.. versionadded:: 0.20.0
517520

518521
``DataFrame`` and ``Series`` will publish a Table Schema representation
519-
by default. This can be disabled globally with the ``display.table_schema``
522+
by default. This can be disabled globally with the ``display.html.table_schema``
520523
option:
521524

522525
.. ipython:: python

doc/source/whatsnew/v0.20.0.txt

+13
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,20 @@ the data.
142142
df
143143
df.to_json(orient='table')
144144

145+
146+
See :ref:`IO: Table Schema for more<io.table_schema>`.
147+
148+
Additionally, the repr for ``DataFrame`` and ``Series`` can now publish
149+
this JSON Table schema representation of the Series or DataFrame if you are
150+
using IPython (or another frontend like `nteract`_ using the Jupyter messaging
151+
protocol).
152+
This gives frontends like the Jupyter notebook and `nteract`_
153+
more flexiblity in how they display pandas objects, since they have
154+
more information about the data.
155+
You must enable this by setting the ``display.html.table_schema`` option to True.
156+
145157
.. _Table Schema: http://specs.frictionlessdata.io/json-table-schema/
158+
.. _nteract: http://nteract.io/
146159

147160
.. _whatsnew_0200.enhancements.other:
148161

pandas/core/config_init.py

+9
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@
164164
(default: False)
165165
"""
166166

167+
pc_table_schema_doc = """
168+
: boolean
169+
Whether to publish a Table Schema representation for frontends
170+
that support it.
171+
(default: False)
172+
"""
173+
167174
pc_line_width_deprecation_warning = """\
168175
line_width has been deprecated, use display.width instead (currently both are
169176
identical)
@@ -339,6 +346,8 @@ def mpl_style_cb(key):
339346
validator=is_bool)
340347
cf.register_option('latex.longtable', False, pc_latex_longtable,
341348
validator=is_bool)
349+
cf.register_option('html.table_schema', False, pc_table_schema_doc,
350+
validator=is_bool)
342351

343352
cf.deprecate_option('display.line_width',
344353
msg=pc_line_width_deprecation_warning,

pandas/core/generic.py

+48-12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import operator
55
import weakref
66
import gc
7+
import json
78

89
import numpy as np
910
import pandas.lib as lib
@@ -129,6 +130,37 @@ def __init__(self, data, axes=None, copy=False, dtype=None,
129130
object.__setattr__(self, '_data', data)
130131
object.__setattr__(self, '_item_cache', {})
131132

133+
def _ipython_display_(self):
134+
try:
135+
from IPython.display import display
136+
except ImportError:
137+
return None
138+
139+
# Series doesn't define _repr_html_ or _repr_latex_
140+
latex = self._repr_latex_() if hasattr(self, '_repr_latex_') else None
141+
html = self._repr_html_() if hasattr(self, '_repr_html_') else None
142+
table_schema = self._repr_table_schema_()
143+
# We need the inital newline since we aren't going through the
144+
# usual __repr__. See
145+
# https://github.com/pandas-dev/pandas/pull/14904#issuecomment-277829277
146+
text = "\n" + repr(self)
147+
148+
reprs = {"text/plain": text, "text/html": html, "text/latex": latex,
149+
"application/vnd.dataresource+json": table_schema}
150+
reprs = {k: v for k, v in reprs.items() if v}
151+
display(reprs, raw=True)
152+
153+
def _repr_table_schema_(self):
154+
"""
155+
Not a real Jupyter special repr method, but we use the same
156+
naming convention.
157+
"""
158+
if config.get_option("display.html.table_schema"):
159+
data = self.head(config.get_option('display.max_rows'))
160+
payload = json.loads(data.to_json(orient='table'),
161+
object_pairs_hook=collections.OrderedDict)
162+
return payload
163+
132164
def _validate_dtype(self, dtype):
133165
""" validate the passed dtype """
134166

@@ -1088,10 +1120,9 @@ def __setstate__(self, state):
10881120
strings before writing.
10891121
"""
10901122

1091-
def to_json(self, path_or_buf=None, orient=None, date_format='epoch',
1092-
timedelta_format='epoch', double_precision=10,
1093-
force_ascii=True, date_unit='ms', default_handler=None,
1094-
lines=False):
1123+
def to_json(self, path_or_buf=None, orient=None, date_format=None,
1124+
double_precision=10, force_ascii=True, date_unit='ms',
1125+
default_handler=None, lines=False):
10951126
"""
10961127
Convert the object to a JSON string.
10971128
@@ -1124,19 +1155,19 @@ def to_json(self, path_or_buf=None, orient=None, date_format='epoch',
11241155
- index : dict like {index -> {column -> value}}
11251156
- columns : dict like {column -> {index -> value}}
11261157
- values : just the values array
1127-
- ``'table'`` dict like
1158+
- table : dict like {'schema': {schema}, 'data': {data}}
11281159
``{'schema': [schema], 'data': [data]}``
1129-
where the schema component is a Table Schema
1130-
describing the data, and the data component is
1131-
like ``orient='records'``.
1160+
the schema component is a `Table Schema_`
1161+
describing the data, and the data component is
1162+
like ``orient='records'``.
11321163
11331164
.. versionchanged:: 0.20.0
11341165
1135-
date_format : {'epoch', 'iso'}
1166+
date_format : {None, 'epoch', 'iso'}
11361167
Type of date conversion. `epoch` = epoch milliseconds,
1137-
`iso` = ISO8601. Default is epoch, except when orient is
1138-
table_schema, in which case this parameter is ignored
1139-
and iso formatting is always used.
1168+
`iso` = ISO8601. The default depends on the `orient`. For
1169+
`orient='table'`, the default is `'iso'`. For all other orients,
1170+
the default is `'epoch'`.
11401171
double_precision : The number of decimal places to use when encoding
11411172
floating point values, default 10.
11421173
force_ascii : force encoded string to be ASCII, default True.
@@ -1155,6 +1186,7 @@ def to_json(self, path_or_buf=None, orient=None, date_format='epoch',
11551186
11561187
.. versionadded:: 0.19.0
11571188
1189+
.. _Table Schema: http://specs.frictionlessdata.io/json-table-schema/
11581190
11591191
Returns
11601192
-------
@@ -1199,6 +1231,10 @@ def to_json(self, path_or_buf=None, orient=None, date_format='epoch',
11991231
"""
12001232

12011233
from pandas.io import json
1234+
if date_format is None and orient == 'table':
1235+
date_format = 'iso'
1236+
elif date_format is None:
1237+
date_format = 'epoch'
12021238
return json.to_json(path_or_buf=path_or_buf, obj=self, orient=orient,
12031239
date_format=date_format,
12041240
double_precision=double_precision,

pandas/tests/formats/test_printing.py

+58
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
from pandas import compat
3+
import pandas as pd
34
import pandas.formats.printing as printing
45
import pandas.formats.format as fmt
56
import pandas.util.testing as tm
@@ -118,6 +119,63 @@ def test_ambiguous_width(self):
118119
self.assertEqual(adjoined, expected)
119120

120121

122+
class TestTableSchemaRepr(tm.TestCase):
123+
124+
@classmethod
125+
def setUpClass(cls):
126+
tm._skip_if_no_mock()
127+
tm._skip_if_no_ipython()
128+
try:
129+
import mock
130+
except ImportError:
131+
from unittest import mock
132+
cls.mock = mock
133+
134+
def test_publishes(self):
135+
df = pd.DataFrame({"A": [1, 2]})
136+
objects = [df['A'], df, df] # dataframe / series
137+
expected_keys = [
138+
{'text/plain', 'application/vnd.dataresource+json'},
139+
{'text/plain', 'text/html', 'application/vnd.dataresource+json'},
140+
]
141+
142+
make_patch = self.mock.patch('IPython.display.display')
143+
opt = pd.option_context('display.html.table_schema', True)
144+
for obj, expected in zip(objects, expected_keys):
145+
with opt, make_patch as mock_display:
146+
handle = obj._ipython_display_()
147+
self.assertEqual(mock_display.call_count, 1)
148+
self.assertIsNone(handle)
149+
args, kwargs = mock_display.call_args
150+
arg, = args # just one argument
151+
152+
self.assertEqual(kwargs, {"raw": True})
153+
self.assertEqual(set(arg.keys()), expected)
154+
155+
with_latex = pd.option_context('display.latex.repr', True)
156+
157+
with opt, with_latex, make_patch as mock_display:
158+
handle = obj._ipython_display_()
159+
args, kwargs = mock_display.call_args
160+
arg, = args
161+
162+
expected = {'text/plain', 'text/html', 'text/latex',
163+
'application/vnd.dataresource+json'}
164+
self.assertEqual(set(arg.keys()), expected)
165+
166+
def test_config_on(self):
167+
df = pd.DataFrame({"A": [1, 2]})
168+
with pd.option_context("display.html.table_schema", True):
169+
result = df._repr_table_schema_()
170+
self.assertIsNotNone(result)
171+
172+
def test_config_default_off(self):
173+
df = pd.DataFrame({"A": [1, 2]})
174+
with pd.option_context("display.html.table_schema", False):
175+
result = df._repr_table_schema_()
176+
self.assertIsNone(result)
177+
178+
121179
# TODO: fix this broken test
122180

123181
# def test_console_encode():

pandas/util/testing.py

+19
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,25 @@ def _skip_if_not_us_locale():
415415
import pytest
416416
pytest.skip("Specific locale is set {0}".format(lang))
417417

418+
419+
def _skip_if_no_mock():
420+
try:
421+
import mock # noqa
422+
except ImportError:
423+
try:
424+
from unittest import mock # noqa
425+
except ImportError:
426+
import nose
427+
raise nose.SkipTest("mock is not installed")
428+
429+
430+
def _skip_if_no_ipython():
431+
try:
432+
import IPython # noqa
433+
except ImportError:
434+
import nose
435+
raise nose.SkipTest("IPython not installed")
436+
418437
# -----------------------------------------------------------------------------
419438
# locale utilities
420439

0 commit comments

Comments
 (0)